[HN Gopher] Ghostling
___________________________________________________________________
Ghostling
Author : bjornroberg
Score : 309 points
Date : 2026-03-20 22:11 UTC (1 days ago)
(HTM) web link (github.com)
(TXT) w3m dump (github.com)
| vintagedave wrote:
| The C file is small enough to read (over a few minutes.)
|
| I got to about line 5 and realized: I've never seen quite that
| technique for embedding a font via an autogenerated header
| before. I'm more used to Windows resources; this seems to
| generate a byte array in CMake code. I'm somewhere between
| horrified and impressed, in that I feel we've finally discovered
| a cross platform binary resource embedding solution.
| vintagedave wrote:
| And as a Windows programmer the use of a method called
| DrawTextEx surprised me :)
|
| A really neat sample. Shows the power of the ghosttty library
| very well. The author chose well with their other libraries,
| it's the kind of demo that lets the code actually demo what to
| trying to without much else getting in the way. Rather
| inspirational to wrote my own terminal app now.
| fresh_broccoli wrote:
| Fun fact: XPM bitmaps were designed to be #included unmodified,
| the files contain C boilerplate:
| https://en.wikipedia.org/wiki/X_PixMap
| simonw wrote:
| Here's the build script that uses: https://github.com/ghostty-
| org/ghostling/blob/main/bin2heade...
|
| I ran it against a 1x1 pixel GIF: cmake
| -DINPUT=pixel.gif -DOUTPUT=pixel.h -DARRAY_NAME=pixel_gif -P
| bin2header.cmake
|
| And got this: // Auto-generated from
| /private/tmp/exp/pixel.gif -- do not edit. static const
| unsigned char pixel_gif[] = { 0x47, 0x49, 0x46, 0x38,
| 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x80, 0x00,
| 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00,
| 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02,
| 0x02, 0x44, 0x01, 0x00, 0x3b };
| flohofwoe wrote:
| Interestingly, cmake 4.3 just added a builtin command
| 'bin2c':
|
| https://cmake.org/cmake/help/v4.3/manual/cmake.1.html#cmdopt.
| ..
|
| ...it will probably take a decade or two until Debian-based
| Linux distros will get cmake 4.3 though ;)
| kingforaday wrote:
| For cross-compilation ease it makes sense if you don't care
| about the size explosion.
| electroly wrote:
| You can use `xxd` from the vim package to generate these.
| You'll find out pretty quickly that this is only suitable for
| small resources: gcc and clang blow up in both time and space
| on very large literal arrays. If you need to ship more than a
| few megabytes, find a different technique.
|
| I used this technique for awhile, but it was too problematic
| for my use case. Now, I use https://github.com/lief-
| project/LIEF -- among other things, this project can modify
| Windows PE, macOS Mach-O, and Linux ELF binaries to add
| resources to them, then offers an API to read them back later.
| It's a little different for each format, but it's capable of
| doing all three and I was able to build a cross-platform
| resource-bundling system that doesn't care how big the
| resources are.
| rweichler wrote:
| Yeah xxd is the correct answer. If you don't want to install
| a dependency and have Lua installed, or if you're just
| feeling a little bit frisky, you can use my function which is
| Production Ready(tm). Xxd = function(name,
| input) if not name:find'^[_%a][_%w]*$' then
| error('bad name: '..tostring(name)) end local ans
| = { 'const unsigned int '..name..'_len =
| '..(#input)..';', 'const unsigned char
| '..name..'[] = {', } local t = {}
| for i=1,#input do table.insert(t,
| ('0x%02x,'):format(input:byte(i))) if #t ==
| 16 then -- 16 columns per row. arbitrary, change this if you
| want table.insert(ans, table.concat(t))
| t = {} end end if #t
| ~= 0 then table.insert(ans, table.concat(t))
| end table.insert(ans, '};\n') return
| table.concat(ans, '\n') end
|
| I am distributing it under the terms of the GNU GPL v3. So if
| you put this in your codebase I will sue you into releasing
| your entire source. Just kidding it's MIT licensed.
|
| Honestly that's a terrible joke. Seriously it's MIT. Here I
| will put the full license in this comment to illustrate how
| serious I am:
|
| Copyright 2026 rweichler
|
| Permission is hereby granted, free of charge, to any person
| obtaining a copy of this software and associated
| documentation files (the "Software"), to deal in the Software
| without restriction, including without limitation the rights
| to use, copy, modify, merge, publish, distribute, sublicense,
| and/or sell copies of the Software, and to permit persons to
| whom the Software is furnished to do so, subject to the
| following conditions:
|
| The above copyright notice and this permission notice shall
| be included in all copies or substantial portions of the
| Software.
|
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
| KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
| WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
| PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
| COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
| OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
| SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
| tjoff wrote:
| Feels like Lua is a more exotic dependency. I used to use
| xxd but this gets problematic when files grow (and they
| don't need to grow much at all), objcopy is much faster
| (which I didn't think would matter much, but it did) and
| don't have the same issues that accidentally opening the .h
| file in your editor or code-searching having to traverse
| that mess.
|
| Yes, you'd want to gitignore it and exclude it from search
| etc. but you still have size issues etc. See _gucci-on-
| fleek_ 's comment on objcopy above for usage.
| rweichler wrote:
| For some things there is nothing better than xxd. It is
| so simple.
| TacticalCoder wrote:
| > I'm somewhere between horrified and impressed, in that I feel
| we've finally discovered a cross platform binary resource
| embedding solution.
|
| Maybe I'm misunderstanding your comment about having "finally
| discovered" that...
|
| For to me embedding binary resources in source files is nothing
| new at all? It was definitely done in the early JavaScript days
| for a variety of reasons.
|
| Arguably early basic listings that had lots of DATA text lines
| were already doing that. Maybe not the most portable but we're
| talking about the 70s and 80s here and definitely binary data
| in source code.
|
| Games for the Atari ST and Amiga, for example, could partially
| share at least some of their source code and it wasn't uncommon
| to encode binary inside sources, including... Fonts! Back then
| fonts were not reused from one game to another.
|
| Heck, I've done in Java (and Java is cross platform) in the
| past: quick visual debug tools for Java GUI apps to toggle a
| debug mode showing something not dissimilar to a HUD. Pixel-
| perfect fonts encoded in .java source files.
|
| I really don't think it's anything new.
|
| P.S: I'm pretty sure it's done like for some fonts in the Linux
| kernel too.
| jrmg wrote:
| `man xxd`
| mitchellh wrote:
| Well, I originally used C23's #embed directive
| (https://en.cppreference.com/w/c/preprocessor/embed) but GCC in
| Nixpkgs doesn't support C23 (or I'm holding it wrong) so I
| dropped back to this. The better long term solution is #embed.
| poly2it wrote:
| I've definitely used C23 via Nixpkgs long ago at this point,
| did you use the `gcc` package and `-std=c23`? Both unstable
| and 25.11 should support it on all currently packaged GCC
| versions.
| conradev wrote:
| I think this has been around for a while: $
| echo 'test' | xxd -i -n foo unsigned char foo[] = {
| 0x74, 0x65, 0x73, 0x74, 0x0a }; unsigned int
| foo_len = 5;
|
| (edit: 30 years)
| w4rh4wk5 wrote:
| For completeness, there's also the somewhat common way of
| creating an object file directly from the binary using objcopy.
| The object file ends up with 3 symbols with names based on the
| input file. For instance, a binary file level0.map yields:
| _binary_level0_map_start, _binary_level0_map_end,
| _binary_level0_map_size.
|
| These can be used in the application through external
| declarations; and you include the object file in the linking
| step, like any other object file.
| gucci-on-fleek wrote:
| My preferred technique (where #embed isn't available) is to use
| objcopy: $ echo 'Hello, world!' > hello-
| world.txt $ objcopy --input-target=binary --output-
| target=elf64-x86-64 \ hello-world.txt hello-world.o
| $ cat <<EOF > embed.c #include <stdint.h>
| #include <stdio.h> #define EMBED(NAME)
| \ extern const uint8_t _binary_##NAME##_start[];
| \ extern const uint8_t _binary_##NAME##_size[];
| #define DATA(NAME) _binary_##NAME##_start #define
| SIZE(NAME) (size_t)_binary_##NAME##_size int
| main() { EMBED(hello_world_txt);
| printf("%.*s", (int)SIZE(hello_world_txt),
| DATA(hello_world_txt)); } EOF $ gcc
| hello-world.o embed.c -o hello-world $ ./hello-world
| Hello, world!
| oDot wrote:
| I use libghostty for Trolley[0], which packages TUIs as desktop
| apps, like Electron does for web apps.
|
| It really is quite an amazing piece of software. I just wrapped
| it in a useful GUI and a bundle/package CLI and it just works.
| Even on Windows. Kudos to the Ghostty developers.
|
| [0] https://github.com/weedonandscott/trolley
| nout wrote:
| I think your github readme is really missing a
| picture/screenshot to quickly understand what is the experience
| like. I.e. if your app is mainly about adding the chrome (as in
| the surrounding UI pixels) around the TUI, then it would be
| good to show what is the chrome like.
| cstrahan wrote:
| Nah, I think it's pretty clear. It would look like a terminal
| emulator. Just like how Electron looks like a bunch of
| browser widgets - because it's literally a single-web-app
| browser.
| nout wrote:
| Does it have any extra controls? And is it using native
| windows (as opposed to Electron)?
| theblazehen wrote:
| A screenshot would still make it clear that that's all
| you're getting - and no extra chrome etc
| theowaway213456 wrote:
| This is a pretty cool idea. Kind of a neat distribution hack if
| all you have is a TUI (and not a full GUI). Curious whether you
| know of any success stories yet
| oDot wrote:
| Thanks.
|
| This was written for my own screenwriting software, which is
| now in private alpha. It works quite nicely for an alpha
|
| https://blisswriter.app
| girvo wrote:
| I kind of want to use this to turn Wordgrinder into a Mac app
| haha
| Imustaskforhelp wrote:
| Oh this is nice!
|
| You mention that Android/IOS support is possible. I hope that
| you can please add these support properly as your project
| matures.
|
| Asking this because I would love to have a cli tool where I can
| just point to for example golang codebase from any device
| (mac/windows/linux) etc. and thanks to golang's cross
| compilation simplicity, have it be compiled for android (well
| linux arm fwiw) and then have it all be compiled into a single
| android apk.
|
| And if you do that, I would love to have something like zenity
| but for android so as to abstract the cli behind a nice gui for
| mass-adoption.
|
| This is almost a million dollar problem as there are so many
| good cli tools and its incredibly easy and versatile to make a
| gui even with scripts on top of that cli but Android/Ios
| usually don't have that versatility.
| imiric wrote:
| This looks interesting.
|
| I don't need my terminal emulator to support tabs, windows, or
| session management. My WM manages tabs and windows, and I use
| tmux for sessions, which also gives me a scrollback buffer,
| selection, clipboard, search, etc. This combination allows me to
| use any simple terminal emulator, such as urxvt, st, and now
| foot, without issues.
|
| Ghostty didn't appeal to me, but I might give this a try. It's
| good that OSC support is planned. A plugin-like system, similar
| to st's but less cumbersome, would be nice to have.
| nixpulvis wrote:
| It's comical how much time I've spent convincing people that
| tabs are a window manager feature not an application feature.
| People in the Alacritty issue on the subject were pissed!
| theowaway213456 wrote:
| Are there any good, non-tiling window managers that support
| tabs? (I struggle with tiling ones like i3 because I am a
| small-brained mouse user)
| nixpulvis wrote:
| I mean, macOS supports tabs now. I wouldn't call it "good"
| though.
| skulk wrote:
| Maybe you'd like Niri?
|
| https://github.com/niri-wm/niri
| ecliptik wrote:
| Fluxbox has tabs and is a stacking window manager.
|
| - https://fluxbox.org/features/
| eddythompson80 wrote:
| Yes, we need tabs for RDR2 and Spotify.
| esperent wrote:
| I would love tabs for Spotify. I just discovered I can at
| least open new windows from the linux YouTube music client
| by middle clicking, a revelation !
| eddythompson80 wrote:
| Every application (or concept) can introduce "tabs", but
| it means something wildly different for that particular
| application. Tabs (or instances) in an application
| immediately bumps into the concept of state (statefull vs
| stateless) in applications.
|
| Sometimes, it makes perfect sense. The reason tabs made
| sense for web browsers since 2004 is because each web
| page could be thought of as a "stateless" instance of an
| application. You're not asking for "tabs", you wish every
| application could be "Stateless". Stateless is a
| beautiful thing, until you understand what state is, and
| who needs to manage it.
|
| If every "tab" of Spotify had no idea what the other
| "tab" is playing and you had to switch back and forth
| between tabs to pause-and-play songs, that would be a
| bug, not a feature. While 2 "windows" playing audio (if
| you instruct them to) is expected.
| esperent wrote:
| > Tabs (or instances) in an application immediately bumps
| into the concept of state (statefull vs stateless) in
| applications.
|
| Agreed, and this is why tabs need to work at the app
| level, not window manager/os level.
|
| That said, for Spotify specifically, it can already tell
| what I'm playing on an entirely different device. I think
| they can handle tabs.
| hombre_fatal wrote:
| I've heard this a lot on HN over the years but it doesn't
| make much sense to me. Some thoughts:
|
| 1. App tabs improves UX for 99.999% of users who aren't using
| a WM with a good tab solution (if one even exists).
|
| 2. WM tabs means launching a new app instance for every tab
| you might want vs having lightweight app tabs.
|
| 3. App tabs can do all sorts of app-level things and UX
| polish that dumb WM tabs can't do because they are so
| general. My terminal emulator tabs show a badge count of bell
| notifications, can be dragged around into groups, or dragging
| into other tabs as split panes. My browser tabs show you
| which tab is playing music and can impl right click -> mute.
|
| 4. I bet even the biggest WM tab cheerleader still uses
| browser tabs.
|
| 5. WM tabs are a different concern than app tabs, not a
| replacement. WM tabs are useful when you want tabs and the
| app doesn't provide a good tab metaphor or when you want to
| tile/group app instances a certain way. That doesn't mean
| it's not useful for the app instances themselves to have app
| tabs when it makes sense.
| monsieurbanana wrote:
| Agree on all the points, except 4. There are even people
| out there who use lynx as their primary browser :)
|
| Although while I usually like tabs for most apps, I don't
| use tabs for terminal and rely either on window manager or
| tmux. I guess the difference is that I often want a mix of
| tabs and having multiple terminals side by side, whereas I
| don't really need that for a browser (or very seldom)
| hombre_fatal wrote:
| Which window manager do you use?
|
| Sway had the better, though often tedious, WM tab
| solution that I've tried. Niri had a useless one.
|
| I really tried to love sway splits and tabs for terminal
| windows. But I finally admitted I'd rather just alt-tab
| to a few different terminal apps, each with its own
| concern (maybe one per project, this one for my remote
| machine), and best of all, each with its own internal
| tabs.
|
| That said, tabs in kitty and tmux, for example, are so
| basic that you don't necessarily lose much if you were to
| use WM tabs instead.
|
| On the other hand, tabs in iTerm2, Ghostty, Cmux,
| probably macOS Terminal -- a bit more powerful and
| intuitive since you can do things like drag them, and
| they can show info like terminal state. And in some of
| those apps, they can be displayed vertically which is my
| favorite.
| rane wrote:
| I like to manage tabs and windows through tmux and it suits
| my workflow very well. What are you going to do now?
| herewulf wrote:
| On tiling WMs I use rxvt-unicode with no window decorations, no
| gaps, 1 px border, no scrollbar. Then tmux does the rest,
| namely tabs and splits. Automatic session saving has been a
| life saver on more than one occasion.
| qudat wrote:
| Interesting you mention tmux because it itself resembles a
| terminal emulator. It has its own terminal feature matrix that
| controls what your parent emulator can render. It sounds like
| you aren't using tabs and splits in tmux but it does include
| them.
|
| It sounds like you could get away with using a tool like
| https://zmx.sh which only handles session persistence
| (attach/detach). It also uses libghostty but only for state
| restoration on reattach.
| mikabasketball wrote:
| A poor man's version of session persistence can even be
| managed with tmux plugins like tmux-resurrect and tmux-
| continuum.
| lzhgusapp wrote:
| I switched to Ghostty a few months ago and it's become one of the
| apps I never close. The rendering speed is noticeably better than
| iTerm2, especially with large log outputs. Excited to see
| libghostty enabling projects like this -- the idea of packaging
| TUIs as native desktop apps is really compelling for indie
| developers.
| lindskogen wrote:
| I have an idea of a terminal emulator where you could maximize
| panes but using a nested structure, does anyone know of one?
|
| Standard "Zoom" features in tmux or iTerm2 only maximize the
| single active pane to the full window, hiding everything else. If
| I have a layout like this: _____________________
| | | B | | A |---------| |
| | C | |_________|_________|
|
| And I expand B, I want A to hide, while B and C remain visible
| together. Then I can create a new nested workspace in there and
| later zoom out when I'm done.
|
| Maybe this could be done arbitrarily deep?
| aquariusDue wrote:
| It's not really what you want but in a similar vein check out:
| zoom.el
|
| https://github.com/cyrus-and/zoom
|
| I guess it's also the kind of thing where screen real-estate is
| a must otherwise when you're at the "top-level" it would look
| weird-ish.
| 0x696C6961 wrote:
| You can do this with i3
| ramon156 wrote:
| Wouldn't you want a hide option rather than a fullscreen
| option? so hide A?
|
| When I use zellij, i just move A to a new tab temporarily
| hombre_fatal wrote:
| This should be simple any time you have panes in a binary tree
| split view.
|
| The hard part is the UX: making it clear that we're in a zoom
| state (esp with nested zooms), somehow showing the split-pane
| tree so that user knows what container will be filled when they
| zoom, etc.
|
| Fork ghostty and get claude to one-shot it so you can see if
| you like the idea.
| znpy wrote:
| I'm seriously interested in this. I wonder if i can use this
| along some decent gui library and an llm to vibe-code a SecureCRT
| replacement.
|
| SecureCRT is awesome but it's crazy expensive :(
| thiht wrote:
| I really hope libghostty succeeds at setting a new baseline for
| what terminal emulation should be. Seems to be on a right track.
| CraigJPerry wrote:
| I used ghostty for the shell in my agent-manager tool (think
| something like https://air.dev/ but in SwiftUI. One architectural
| detail i'm still going back and forth on is: who should own the
| PTY?
|
| If embedded Ghostty owns it, now i have to have some kind of
| workaround to instrument what's happening inside the terminal -
| signals, stdout/stderr events, return code etc.
|
| If my parent agent-manager app owns it, now i don't have the nice
| clean small interface to ghostty (i'm a fan of John Ousterhout
| style narrow but deep interfaces, pulling complexity down rather
| than pushing up to parent).
|
| Not sure if any other ghostty embedders might have advice. It's
| on my todo list to task a an agent to "gh repo clone" a few
| ghostty using shells and report on their arch.
| willrshansen wrote:
| Sounds like you need to multiplex a terminal with a terminal
| multiplexer.
|
| terminal multiplexer
|
| term mux
|
| tmux
| delduca wrote:
| I have plans to add it to my game (SDL).
| hombre_fatal wrote:
| I built my own macOS terminal app over the last two weeks using
| swift/appkit + ghostty's zig library.
|
| I basically just wanted vertical tabs and notifications for when
| AI agents (claude code, codex) are finished.
|
| I already use it as my main terminal over iTerm.
|
| It's a fun project since I use my terminal all day, so I always
| have ideas for how something could be improved or polished. AI
| can do the chore work of figuring out how to impl some bugfix or
| UX polish, and I manage the last 10%.
|
| This would have been too much work to maintain for fun before
| LLMs.
| vunderba wrote:
| FWIW most of the agentic CLI tools also let you setup
| notification hooks so you can just do something like this:
| afplay /System/Library/Sounds/Glass.aiff
| osascript -e 'display notification "Waiting..." with title
| "Coding Agent"
|
| The sound is especially useful since for longer running tasks
| I'll often do some chores around the house while waiting for it
| to finish up.
| hombre_fatal wrote:
| My notification hook pipes an OSC 777 (title, body) message
| into /dev/tty, Ghostty handles/ingests it and then emits the
| event for my terminal app to do things like craft a macOS
| notif and style the tab/pane of the originating terminal.
| #!/usr/bin/env bash MSG=$(cat | jq -r
| '.last_assistant_message // empty' | head -c 200)
| printf '\e]777;notify;Claude Code;%s\a' "${MSG:-Claude
| finished responding}" > /dev/tty
|
| I tried the osascript solution first but had some issues,
| iirc no good way to focus the originating terminal pane on
| notif click.
|
| Something I never figured out is why Claude Code's
| "Notification" hook waits minutes to fire. I had to use the
| "Stop" hook for actual end-of-response timing.
| vunderba wrote:
| _> iirc no way to focus the originating terminal pane on
| notif click._
|
| Yeah mine has the same issue - it just brings up the script
| editor. It's not as much of an issue for me since I'm
| rarely running more than a single Opencode instance at a
| time.
|
| Could definitely see that being useful if you were running
| quite a few agents to let you hone in on the correct
| terminal window/tab quickly though.
| hombre_fatal wrote:
| Yeah, exactly. I have so many agents running these days.
| 1-2 per project.
|
| Often one is polishing a plan file for the next
| feature/bugfix (claude), and the other is reviewing it to
| give feedback (codex). And that is my life now. But on
| the upside, it's trivial to maintain sideprojects now.
| isjdiwjxiwj wrote:
| I hardly think you can say YOU built it went all your did was
| manage 10% of the project. An LLM built it, you just test it.
| hombre_fatal wrote:
| Figuring out what to build, how it should work at a high
| level, and what it should be like to use it is the hard part
| and the defining aspect of any tool or application. (Or a fun
| game, for example)
|
| Now that the cost of writing code is ~$0, I think the entity
| who figures that out is the one who built it, not the entity
| that does the trivial chore once the hard decisions have
| already been made.
| echelon wrote:
| I love seeing an `AGENTS.md` in open source projects.
|
| It's now my #1 heuristic to know if the team is on the right
| track.
|
| (I need to start adding them to all my projects.)
| octetta wrote:
| This is amazing and might be exactly what I'm looking for my own
| weirdo retro tooling that sometimes needs to run over ssh but
| also expect a "GUI" experience... any metrics on the overhead
| this might add to, for instance to a hello world type program?
___________________________________________________________________
(page generated 2026-03-21 23:00 UTC)