Terminal showdown: Kitty beats Ghostty for aterm, and the deciding factor is the process model, not the features #1264

Closed
opened 2026-08-26 03:14:52 +00:00 by coilyco-ops · 3 comments
Member

Part of #1245. Both installed on the Mac daily driver and tested against aterm's real launch plan rather than compared from feature lists. Ghostty 1.3.1, kitty 0.48.2, against alacritty 0.17.0 as the incumbent baseline.

Method

A probe script runs inside each launched window and reports what the terminal actually applied, by querying OSC 11 (background) and OSC 12 (cursor) from its own tty. That verifies branding end to end instead of trusting that a flag was accepted.

Scoreboard

                              alacritty 0.17   kitty 0.48.2   ghostty 1.3.1
per-window brand at launch         yes            yes            yes
  verified via OSC 11 readback     #211b28        #211b28        #211b28
concurrent distinct role windows   yes            yes            NOT REACHED
launches from CLI on macOS         yes            yes            documented unsupported
child's parent process             alacritty      kitty          /usr/bin/login
detached spawn (Setsid, aterm's)   yes            yes            HUNG, 4 dead procs
answers OSC 12 cursor query        no             yes            yes
background image                   no             yes            yes (richer controls)
title bar background color         no             yes            yes
kitty graphics protocol            no             native         implements it
live re-brand from outside         no             VERIFIED       no remote control
Windows                            yes            no             no

Why Ghostty loses, and it is not about features

Ghostty's own --help says it: "On macOS, launching the terminal emulator from the CLI is not supported and only actions are supported. Use open -na Ghostty.app instead."

That is aimed directly at spawnWindow. aterm starts the terminal detached with Setsid: true, watches it for 400 ms, and reports a startup failure with the cause. Testing that exact pattern produced four live Ghostty processes with windows open that never ran the command, no stderr, and nothing to report. Run in the foreground it works, and then it blocks until the session ends, which inverts the earlyExitWindow heuristic whose comment records that "Alacritty hands off and exits 0 at once on macOS."

The probe also showed ppid_chain=/usr/bin/login under Ghostty, against alacritty and kitty under the other two. The child is decoupled from the launcher, so aterm cannot observe a startup failure at all. That kills the two properties docs/aterm.md sells: a failing launch stays on screen, and "no window appeared" comes back naming the cause.

Ghostty is the better-looking terminal and its background image controls are genuinely richer (fit, position, repeat, opacity against kitty's layout, linear, tint). It is the wrong shape for a launcher whose entire job is spawn-and-watch.

Why Kitty wins

  1. The swap is nearly textual. -o name=value is byte-identical to Alacritty's flag shape, -T/--title overrides and permanently fixes the title exactly as window.dynamic_title=false does, -d is --working-directory, trailing args replace -e. buildLaunchPlan changes shape barely at all.
  2. #1258 is delivered by the terminal. Verified end to end: launched a window at #211b28, ran kitten @ --to unix:... set-colors background='#331116' from an outside process, and the probe inside the window reported the change. A brand that is alive needs no hand-rolled control socket in _session, no OSC plumbing, no harness cooperation beyond poking a socket.
  3. State is queryable. kitty answers OSC 12; Alacritty returns empty. That makes the branding assertions in #1259 testable rather than assumed.
  4. #1261 unblocks. background_image, background_image_layout, background_image_linear, background_tint, plus window_title_bar_active_background, which is a per-role branding channel Alacritty does not have at all.
  5. #1255 gets real pixels. Native kitty graphics protocol, so the emblem stops being half-blocks.
  6. --session files exist if crews ever come back.

The one real cost

Neither kitty nor Ghostty ships on Windows. Alacritty does.

aterm builds for Windows and the release pipeline renders Scoop metadata for it, so moving to kitty means Windows aterm has no terminal. That reopens the adapter question that was deliberately closed: kitty on macOS and Linux plus Alacritty on Windows is two adapters, which is the interface that got cut.

Three honest ways out, and this is the decision the swap actually turns on:

  • Windows aterm is aspirational rather than used, so drop it and go kitty-only
  • keep the two-terminal split and accept a minimal adapter interface after all, scoped to exactly two
  • stay on Alacritty and take the half-block splash in #1261

Correction to an earlier claim in this epic

I previously wrote that platform_windows.go meant Windows was "a target that cannot open a window." That was wrong, and it was an over-read of detachAttr returning nil there. Alacritty ships Windows builds and aterm builds for Windows. detachAttr returning nil only means no setsid equivalent, not no window. Corrected in #1245 and #1261.

Housekeeping

Ghostty and kitty are now installed on the Mac daily driver. Both are brew uninstall --cask away if this does not go anywhere.

Part of #1245. Both installed on the Mac daily driver and tested against aterm's real launch plan rather than compared from feature lists. Ghostty 1.3.1, kitty 0.48.2, against alacritty 0.17.0 as the incumbent baseline. ## Method A probe script runs inside each launched window and reports what the terminal actually applied, by querying OSC 11 (background) and OSC 12 (cursor) from its own tty. That verifies branding end to end instead of trusting that a flag was accepted. ## Scoreboard ``` alacritty 0.17 kitty 0.48.2 ghostty 1.3.1 per-window brand at launch yes yes yes verified via OSC 11 readback #211b28 #211b28 #211b28 concurrent distinct role windows yes yes NOT REACHED launches from CLI on macOS yes yes documented unsupported child's parent process alacritty kitty /usr/bin/login detached spawn (Setsid, aterm's) yes yes HUNG, 4 dead procs answers OSC 12 cursor query no yes yes background image no yes yes (richer controls) title bar background color no yes yes kitty graphics protocol no native implements it live re-brand from outside no VERIFIED no remote control Windows yes no no ``` ## Why Ghostty loses, and it is not about features Ghostty's own `--help` says it: "On macOS, launching the terminal emulator from the CLI is not supported and only actions are supported. Use `open -na Ghostty.app` instead." That is aimed directly at `spawnWindow`. aterm starts the terminal detached with `Setsid: true`, watches it for 400 ms, and reports a startup failure with the cause. Testing that exact pattern produced **four live Ghostty processes with windows open that never ran the command, no stderr, and nothing to report.** Run in the foreground it works, and then it blocks until the session ends, which inverts the `earlyExitWindow` heuristic whose comment records that "Alacritty hands off and exits 0 at once on macOS." The probe also showed `ppid_chain=/usr/bin/login` under Ghostty, against `alacritty` and `kitty` under the other two. The child is decoupled from the launcher, so aterm cannot observe a startup failure at all. That kills the two properties `docs/aterm.md` sells: a failing launch stays on screen, and "no window appeared" comes back naming the cause. Ghostty is the better-looking terminal and its background image controls are genuinely richer (`fit`, `position`, `repeat`, `opacity` against kitty's `layout`, `linear`, `tint`). It is the wrong shape for a launcher whose entire job is spawn-and-watch. ## Why Kitty wins 1. **The swap is nearly textual.** `-o name=value` is byte-identical to Alacritty's flag shape, `-T/--title` overrides and permanently fixes the title exactly as `window.dynamic_title=false` does, `-d` is `--working-directory`, trailing args replace `-e`. `buildLaunchPlan` changes shape barely at all. 2. **#1258 is delivered by the terminal.** Verified end to end: launched a window at `#211b28`, ran `kitten @ --to unix:... set-colors background='#331116'` from an outside process, and the probe inside the window reported the change. A brand that is alive needs no hand-rolled control socket in `_session`, no OSC plumbing, no harness cooperation beyond poking a socket. 3. **State is queryable.** kitty answers OSC 12; Alacritty returns empty. That makes the branding assertions in #1259 testable rather than assumed. 4. **#1261 unblocks.** `background_image`, `background_image_layout`, `background_image_linear`, `background_tint`, plus `window_title_bar_active_background`, which is a per-role branding channel Alacritty does not have at all. 5. **#1255 gets real pixels.** Native kitty graphics protocol, so the emblem stops being half-blocks. 6. `--session` files exist if crews ever come back. ## The one real cost **Neither kitty nor Ghostty ships on Windows. Alacritty does.** aterm builds for Windows and the release pipeline renders Scoop metadata for it, so moving to kitty means Windows aterm has no terminal. That reopens the adapter question that was deliberately closed: kitty on macOS and Linux plus Alacritty on Windows is two adapters, which is the interface that got cut. Three honest ways out, and this is the decision the swap actually turns on: - Windows aterm is aspirational rather than used, so drop it and go kitty-only - keep the two-terminal split and accept a minimal adapter interface after all, scoped to exactly two - stay on Alacritty and take the half-block splash in #1261 ## Correction to an earlier claim in this epic I previously wrote that `platform_windows.go` meant Windows was "a target that cannot open a window." That was wrong, and it was an over-read of `detachAttr` returning nil there. Alacritty ships Windows builds and aterm builds for Windows. `detachAttr` returning nil only means no setsid equivalent, not no window. Corrected in #1245 and #1261. ## Housekeeping Ghostty and kitty are now installed on the Mac daily driver. Both are `brew uninstall --cask` away if this does not go anywhere.
Author
Member

Decided: kitty. Landed in #1266, squashed to main as bc5c9863.

What shipped

  • --working-directory becomes --directory, program moves to the trailing arguments since kitty has no -e
  • window.dynamic_title=false is gone, because kitty's --title permanently fixes the OS window title against the child
  • brand colors move to background, cursor, selection_background, selection_foreground
  • --alacritty-bin becomes --terminal-bin, reading ATERM_TERMINAL_BIN or KITTY_BIN
  • new kitty/kitty.conf carries the Sombra baseline. kitty has its own tab surface, so hiding the bar was not enough on its own and each shortcut is unmapped rather than left to a hidden bar
  • aos-cli terminal detection now names KITTY_WINDOW_ID alongside ALACRITTY_WINDOW_ID

What the swap exposed

Nothing asserted the brand survived into the terminal's arguments, so a flag-dialect change could have dropped every color and still passed the entire suite. Both a new unit test and the release gate in scripts/check-aos-release.sh now check it. That gate would also have failed silently on assert plan["executable"] == "alacritty".

Verified end to end

A probe inside a real launched window, through aterm's own _session stage, reported background #211b28 and cursor #e583f7, matching the composed frontend brand. Session stage exit semantics re-checked: clean child 0, child exiting 3 gives 3 and holds, missing binary gives 1 and holds naming the cause.

Still open

Windows. alacritty/alacritty.toml is retained as the Windows baseline, but --terminal-bin has no Alacritty flag dialect, so pointing it there does not work. The decision recorded above stands unanswered: either Windows aterm is aspirational and gets dropped, or a two-terminal adapter comes back scoped to exactly two. Leaving this issue open to carry that.

#1261 is now unblocked on the terminal side and still correctly waits on agent-compose#358.

Decided: kitty. Landed in #1266, squashed to `main` as `bc5c9863`. ## What shipped - `--working-directory` becomes `--directory`, program moves to the trailing arguments since kitty has no `-e` - `window.dynamic_title=false` is gone, because kitty's `--title` permanently fixes the OS window title against the child - brand colors move to `background`, `cursor`, `selection_background`, `selection_foreground` - `--alacritty-bin` becomes `--terminal-bin`, reading `ATERM_TERMINAL_BIN` or `KITTY_BIN` - new `kitty/kitty.conf` carries the Sombra baseline. kitty has its own tab surface, so hiding the bar was not enough on its own and each shortcut is unmapped rather than left to a hidden bar - `aos-cli` terminal detection now names `KITTY_WINDOW_ID` alongside `ALACRITTY_WINDOW_ID` ## What the swap exposed Nothing asserted the brand survived into the terminal's arguments, so a flag-dialect change could have dropped every color and still passed the entire suite. Both a new unit test and the release gate in `scripts/check-aos-release.sh` now check it. That gate would also have failed silently on `assert plan["executable"] == "alacritty"`. ## Verified end to end A probe inside a real launched window, through aterm's own `_session` stage, reported `background #211b28` and `cursor #e583f7`, matching the composed frontend brand. Session stage exit semantics re-checked: clean child 0, child exiting 3 gives 3 and holds, missing binary gives 1 and holds naming the cause. ## Still open **Windows.** `alacritty/alacritty.toml` is retained as the Windows baseline, but `--terminal-bin` has no Alacritty flag dialect, so pointing it there does not work. The decision recorded above stands unanswered: either Windows aterm is aspirational and gets dropped, or a two-terminal adapter comes back scoped to exactly two. Leaving this issue open to carry that. #1261 is now unblocked on the terminal side and still correctly waits on agent-compose#358.
Author
Member

The unanswered Windows question is not neutral. It is currently shipping broken. Verified on main at d24e5d5a.

The release pipeline still builds and publishes the Windows binary:

  • scripts/aos-release-build.sh - build_aterm still takes a windows/* target and emits aterm-windows-amd64.exe.
  • scripts/render-aos-packaging.sh - still checksums aterm-windows-amd64.exe, still maps it into the Scoop manifest as aterm, still renders the PowerShell fetch.

And docs/install.md:83 now says outright: "kitty does not ship on Windows, so Alacritty keeps the baseline there."

So a Windows host installs aterm from Scoop, and aterm <role> <seat> defaults --terminal-bin to kitty, which is not installable there. Pointing it at Alacritty does not work either, because the plan is built in kitty's flag dialect: --title, --directory, program in the trailing arguments with no -e. There is no configuration a Windows operator can reach that produces a window.

That was an accepted open question when it read "Windows aterm may be aspirational". It reads differently now that the artifact is still being published every release into a Scoop bucket. Whichever of the three ways out gets picked, the packaging has to move with it, and that is the part no option in the body currently mentions:

  • drop Windows aterm - remove the windows/* target from build_aterm and the aterm entries from the Scoop manifest, so nothing installs a launcher that cannot launch
  • two-terminal adapter scoped to exactly two - plan.go grows a dialect seam, packaging stays as-is
  • leave it - then docs/install.md and the Scoop manifest should at minimum say the Windows binary opens no window today

Recording the packaging consequence here since this issue is the one carrying the decision. Nothing else in the #1245 batch touches it.

**The unanswered Windows question is not neutral. It is currently shipping broken.** Verified on `main` at `d24e5d5a`. The release pipeline still builds and publishes the Windows binary: * `scripts/aos-release-build.sh` - `build_aterm` still takes a `windows/*` target and emits `aterm-windows-amd64.exe`. * `scripts/render-aos-packaging.sh` - still checksums `aterm-windows-amd64.exe`, still maps it into the Scoop manifest as `aterm`, still renders the PowerShell fetch. And `docs/install.md:83` now says outright: "kitty does not ship on Windows, so Alacritty keeps the baseline there." So a Windows host installs `aterm` from Scoop, and `aterm <role> <seat>` defaults `--terminal-bin` to `kitty`, which is not installable there. Pointing it at Alacritty does not work either, because the plan is built in kitty's flag dialect: `--title`, `--directory`, program in the trailing arguments with no `-e`. There is no configuration a Windows operator can reach that produces a window. That was an accepted open question when it read "Windows aterm may be aspirational". It reads differently now that the artifact is still being published every release into a Scoop bucket. Whichever of the three ways out gets picked, **the packaging has to move with it**, and that is the part no option in the body currently mentions: * drop Windows aterm - remove the `windows/*` target from `build_aterm` and the aterm entries from the Scoop manifest, so nothing installs a launcher that cannot launch * two-terminal adapter scoped to exactly two - `plan.go` grows a dialect seam, packaging stays as-is * leave it - then `docs/install.md` and the Scoop manifest should at minimum say the Windows binary opens no window today Recording the packaging consequence here since this issue is the one carrying the decision. Nothing else in the #1245 batch touches it.
Author
Member

Decided: drop the Windows build

Kai settled this 2026-08-25. Windows aterm goes away rather than growing a second terminal dialect. The adapter stays cut.

The reasoning on record: a launcher that cannot launch is worse in a Scoop bucket than absent from one, and the removal is reversible. If Windows aterm turns out to be wanted, the two-terminal adapter scoped to exactly two is the way back, and this issue records that it was priced.

What that makes concrete

  • scripts/aos-release-build.sh - drop the windows/* target from build_aterm, so no aterm-windows-amd64.exe is produced.
  • scripts/render-aos-packaging.sh - remove the aterm checksum line, the ["aterm-windows-amd64.exe", "aterm"] manifest mapping, and the PowerShell fetch block.
  • scripts/check-aos-release.sh - drop any Windows aterm assertion so the gate matches what ships.
  • docs/install.md:83 - the sentence "kitty does not ship on Windows, so Alacritty keeps the baseline there" is now about Alacritty as a general Windows terminal baseline, not about aterm. Say plainly that aterm is Mac and Linux.
  • aterm/platform_windows.go - keep or remove is the implementer's call. It costs nothing and removing it is tidier now that nothing builds for that target. alacritty/alacritty.toml stays, since it is still the Windows terminal baseline for everything that is not aterm.
  • docs/FEATURES.md - aterm's entry has a platform boundary now, which is a material change to its public surface.

Downstream effect on the batch

#1257 and #1259 are now unambiguously unix-only. Doctor reports one terminal, the resolved --terminal-bin, with no per-platform branch. The contract tests walk one dialect.

This issue closes when the packaging change lands, not before, since the whole point is that the artifact stops shipping.

## Decided: drop the Windows build Kai settled this 2026-08-25. Windows aterm goes away rather than growing a second terminal dialect. The adapter stays cut. The reasoning on record: a launcher that cannot launch is worse in a Scoop bucket than absent from one, and the removal is reversible. If Windows aterm turns out to be wanted, the two-terminal adapter scoped to exactly two is the way back, and this issue records that it was priced. ### What that makes concrete * `scripts/aos-release-build.sh` - drop the `windows/*` target from `build_aterm`, so no `aterm-windows-amd64.exe` is produced. * `scripts/render-aos-packaging.sh` - remove the aterm checksum line, the `["aterm-windows-amd64.exe", "aterm"]` manifest mapping, and the PowerShell fetch block. * `scripts/check-aos-release.sh` - drop any Windows aterm assertion so the gate matches what ships. * `docs/install.md:83` - the sentence "kitty does not ship on Windows, so Alacritty keeps the baseline there" is now about Alacritty as a general Windows terminal baseline, not about aterm. Say plainly that aterm is Mac and Linux. * `aterm/platform_windows.go` - keep or remove is the implementer's call. It costs nothing and removing it is tidier now that nothing builds for that target. `alacritty/alacritty.toml` stays, since it is still the Windows terminal baseline for everything that is not aterm. * `docs/FEATURES.md` - aterm's entry has a platform boundary now, which is a material change to its public surface. ### Downstream effect on the batch #1257 and #1259 are now unambiguously unix-only. Doctor reports one terminal, the resolved `--terminal-bin`, with no per-platform branch. The contract tests walk one dialect. This issue closes when the packaging change lands, not before, since the whole point is that the artifact stops shipping.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-flight-deck/agentic-os#1264
No description provided.