Files
ELYTerminal/AGENTS.md
T

68 KiB
Raw Blame History

AGENTS.md

ELYTerminal. A terminal on GPUI and Ely components. PRD.md says what it does; TASKS.md says how far it is; tasks/acceptance.md checks the PRD line by line.

Stack

  • Rust 1.95.0, as rust-toolchain.toml pins it, edition 2024. One binary crate, elyterminal.
  • Ely (ely-gpui-component) from git.zacharyzhang.com, pinned by rev. gpui and gpui_platform from crates.io's gpui-pre, the exact version Ely pins, under their crate names through package.
  • alacritty_terminal 0.26: grid, parser, pseudo-terminal, event loop.
  • serde, serde_json: settings. dirs: the config folder. base64, the version alacritty pulls in for OSC 52: OSC 7501's text. log, env_logger: logs to stderr. unicode-normalization, on macOS: Git's marks in composed form.
  • On Linux, wayland-client, wayland-backend and wayland-protocols (staging) ask the compositor for ext_background_effect_v1 on gpui's own connection; wayland-server hosts a compositor in the tests. x11rb sets _KDE_NET_WM_BLUR_BEHIND_REGION; its test runs Xvfb.
  • On Windows, sysinfo reads herdr's client's arguments and variables; windows-sys, the version the Windows build already holds, lists processes, waits on herdr's pipe, reads the keyboard for win32-input-mode, and keeps Ely's window on the main one, which raw-window-handle finds.
  • resvg, a dev dependency at the version gpui uses, renders the icon's SVG for the macOS app and the Windows .ico. embed-resource, a build dependency at the version gpui uses, puts the icon into the Windows executable.

Commands

  • cargo run opens the app. RUST_LOG=elyterminal=debug cargo run logs more; trace logs each paint.
  • cargo run --example shot -- [--keys "SPECS"] <png> <program> [args…] runs a program in the main window, plays SPECS on it, and writes a PNG of the window, and one of each child window beside it, Ely's among them, with each one's frame in the log (macOS; the app it runs under needs Screen Recording, or the picture may hold the window's ground alone, and shot warns), naming the AppKit views under its content, an effect view with its blur radius, none when it draws no blur, and a capture of one window shows its alpha, not the blur behind it; it logs the menu bar, each item with its key. It shows its window without activating the app, so keystrokes or clicks meant for other apps stay there; gpui draws a window only while macOS shows it, so one on another Space or under a full-screen app keeps its last frame. Run it with HOME pointing at a temp folder to try settings without touching your own. SPECS, space-separated:
    • keys as real AppKit events: a, ctrl-c, lopt-f and ropt-f (left and right Option), cmd-v, repeat-up (AppKit's repeat flag); down: or up: before a key posts only its press or its release, so a modifier may change between them;
    • input method calls: mark:é composes, commit:é types;
    • wait pauses a second, so a program can start before the keys after it;
    • activate and deactivate the app, which takes real keystrokes while active: keep the two specs adjacent. macOS may delay or refuse the activation; each shot: typed line says whether the app was active;
    • to:settings and to:main aim the specs after them, and the picture, at that window, which becomes the app's key window, since AppKit sends keys there; the app stays inactive, and gpui sends an inactive window no focus or blur events, so press Enter to commit a field. AppKit gives an inactive app's key that a menu item holds to the menu, and gpui sends the item's action to the main window, which an inactive app lacks: only an app-wide action runs, such as Quit, Settings or the font's size;
    • mouse gestures handed to gpui's view, at points from the content's top-left, modifiers as for keys: click@x,y, click2@, click3@, rclick@, mclick@, drag@x,y@x,y, move@x,y, the steps ldown@, lup@, ldrag@, rdown@, rup@, mdown@, mup@, and scrollup@x,y@lines, scrolldown@x,y@lines.
  • scripts/check.sh: fmt, clippy -D warnings, tests, the 500-line rule.
  • scripts/linux.sh ARGS runs cargo ARGS on the working tree in a Linux container, the build kept in Docker volumes; scripts/linux.sh test --locked runs the tests as a user, not root, as CI does. The tree goes in as a tar stream, every file dated now: Colima's shared folder can show the VM a file as it was before an edit, and cargo judges by date, so a tree older than the last build would reuse that build. It needs a Docker daemon: on macOS, Colima (colima start).
  • scripts/mac-app.sh builds target/release/ELYTerminal.app from a release build, signed ad hoc, and ELYTerminal-VERSION.dmg beside it, the app next to a link to Applications. Given SIGN_IDENTITY and the App Store Connect key, NOTARY_KEY (its .p8 file), NOTARY_KEY_ID and NOTARY_ISSUER, it signs with that identity under the hardened runtime, then notarizes and staples the image, and it stops before building when one is missing or no keychain holds the identity. Its icon: examples/icons.rs renders assets/icons/elyterminal-macos.svg into an iconset, which iconutil packs.
  • scripts/windows-zip.ps1, on Windows with Visual Studio's C++ tools, builds elyterminal.exe with its icon and zips it as ELYTerminal-VERSION-windows-ARCH.zip in target/release. After an edit to the figure, cargo run --example icons -- ico assets/icons/elyterminal.svg assets/icons/elyterminal.ico renders the icon again.
  • scripts/linux-tar.sh builds the release in Docker on rust:1.95.0-bookworm and packs it with com.zacharyzhang.elyterminal.desktop and the icon as ELYTerminal-VERSION-linux-ARCH.tar.gz in target/release.
  • scripts/release-asset.sh FILE puts FILE on the Gitea release of this version, a draft too, in place of a file of its name; RELEASE_API and RELEASE_TOKEN say where and as whom. The Release workflow, .github/workflows/release.yml, dispatched by hand, builds the signed and notarized disk image on the macos-signing runner and puts it on the release.

Layout

  • build.rs: on Windows, the icon into the executable as resource 1, through assets/icons/elyterminal.rc. .cargo/config.toml: Windows builds carry the C runtime. rust-toolchain.toml: the toolchain rustup installs.
  • src/main.rs: start. src/lib.rs: the modules, public so tests and examples reach them. src/app.rs: fonts, settings, integration, then the main window, at the size the settings keep, which it keeps as it settles. src/menus.rs: the menu bar, the keys of Quit, the font's size and Minimize, and what they do. src/blur.rs: what the window shows of the desktop, from the opacity and blur settings, set as they change; blur/macos gives gpui's NSVisualEffectView a material that blurs; blur/wayland asks a Wayland compositor for blur behind the window's surface through ext_background_effect_v1; blur/x11 names the window's pane in _KDE_NET_WM_BLUR_BEHIND_REGION. src/workspace.rs: the window's content, Ely's ColumnShell with the sessions, terminal and right columns, their toggles, the right column's Git and Files switch, the title, and the sessions' lifecycle. src/workspace/widths.rs: the side columns' widths, which a drag on their border sets and the settings keep once it rests. src/workspace/events.rs: what a session's terminal reports: title, folder, bell, the state its row shows, the program's exit; and a press on a notification a session posted. src/sessions.rs: a session and its row, with the mark of its status and what hover shows of it; sessions/orb draws the mark, 3 by 3 dots on one clock. src/files.rs: the Files panel, the shown session's folder as Ely's Tree with Git's marks. src/git.rs: git through its command line: Files' marks, and the Git panel's status, stage, unstage, discard, commit and log; src/git/diff.rs reads git's diff into Ely's stretches. src/git/panel.rs: the Git panel's state and actions; src/git/panel/view.rs: how it draws.
  • src/pet.rs: Ely, peeking over the main window's top edge from a window of her own while the settings want her; pet/faces draws her faces as SVG from the icon's paths, pet/look reads her mood from the sessions' marks and the face she makes at each moment of it, and pet/macos, pet/windows and pet/x11 keep her window on the main window's edge where the platform does not.
  • src/settings: the model, its ranges, and SettingsStore, the global that loads, checks and saves settings.json; window is the settings window, window/appearance its Appearance page, window/terminal its Terminal page. tests/fonts.rs brings its own main: gpui builds its Mac platform only on the main thread.
  • src/themes: built-in terminal themes, the chrome palette each maps to, and apply, which dresses the app for the system's appearance; omarchy, on Linux, wears Omarchy 4's current theme and follows its switches.
  • src/term: the terminal engine. pty starts a program through alacritty's event loop; tap wraps the pty so its reader scans OSC 0, 2, 7, 9, 99, 133, 777 and 7501, the title stack's CSI 22 t and 23 t, full resets, the mouse modes and, on Windows, win32-input-mode (tap/modes) before the parser; titles keeps the title a session shows, read in the output's order; marks reads OSC 133's prompt marks; status reads OSC 7501 and keeps a terminal's records; notify reads OSC 9, 777 and 99, the notifications and OSC 9;4's progress, and gathers OSC 99's chunks; reports is the view's side of what programs report: it keeps OSC 7501's records, maps OSC 9;4, answers OSC 99's query and alive, and posts the system notifications its session posts; prompts turns each mark into an OSC 8 link the parser keeps in a prompt's cells, and finds the prompt to jump to; herdr follows herdr's focused pane and its agents' states through herdr's socket while herdr's client runs in the session, and tells the view herdr leads, so herdr opens its links: herdr/unix finds the client leading the pty's foreground, herdr/windows under the session's shell, and asks its named pipe. unix starts the program itself, so a failed chdir fails the start; Windows uses alacritty's ConPTY. shell writes the integration scripts at start and builds each shell's launch. frame reads the grid into styled rows, boxes turns box drawing, block elements and powerline arrows into rects and polygons (boxes/outline strokes curves and diagonals into polygons), ink resolves colors, paint draws rows, decorations and the cursor, view is the entity: pty events, metrics, blink, resize. zoom holds the points Bigger and Smaller add to the Size setting, unsaved. keys turns key events into bytes, the kitty protocol and, in keys/legacy, xterm's encoding, with no gpui types; input is the view's key listeners, the bindings that keep Tab and F6 from Ely's focus moves, and its IME handler; mac reads the Option side, the repeat flag and the key code of AppKit's current event, and the key's own character from the layout, as gpui reads key names; it keeps held keys repeating, and hands Command key-ups to the focused view. win32 sends a key as Windows' own record, win32-input-mode, and finds a key's own character in Windows' layout. mouse encodes mouse reports, with no gpui types; links finds the link at a cell; pointer is the view's mouse listeners: selection, reports, wheel, links, copy and paste. find is the find box: Ely's FindWidget, and Ely's find_all over the grid's shown text.
  • assets/shell-integration: ELYTerminal's own scripts (not Ghostty's, which are GPL): zsh through ZDOTDIR, bash through --rcfile, fish through XDG_DATA_DIRS, PowerShell through -NoExit -Command. Each prompt sends OSC 7 and, except in fish, whose fish_title stays, a title; each marks prompts and commands with OSC 133 where the shell does not.
  • assets/fonts: JetBrains Mono 2.304's bold and italic faces (OFL; Ely bundles regular), and Nerd Fonts 3.5.1's JetBrains Mono NF Mono Regular, which draws icons a font lacks.
  • assets/themes: Ghostty theme files copied verbatim from iTerm2-Color-Schemes (MIT, its LICENSE beside them).
  • assets/icons: the icon, an original figure, Ely, holding a terminal: elyterminal-macos.svg on macOS's rounded square, elyterminal.svg the figure alone, for Windows and Linux; the macOS file holds a copy of the figure's markup, so an edit to the figure is made in both. elyterminal.ico is the figure at 16 to 256 px for Windows, and elyterminal.rc names it resource 1.

Rules

  • Hand-written files stay at or under 500 lines; Cargo.lock, which Cargo writes, is exempt. Comments are one short line, and rare.
  • Fail fast. No silent fallbacks. An error names its owner and what failed.
  • Log every state change on a key path with log: spawn, exit, folder change, settings load and save, blur path.
  • No dead code, no versioned names, no compatibility shims.
  • Colors, sizes and radii come from Ely's theme. Gaps use gpui's rem helpers.
  • No credential in any file. Pushes pass the token as an http.extraHeader on the command line.

Decisions

  • 2026-10-14: ELYTerminal owns its terminal engine on alacritty_terminal. Ely's Terminal lacks mouse reporting, IME, kitty keys and style settings, and a library widget should not carry an app's engine.
  • 2026-10-14: the working directory comes from OSC 7, read by a tap on the pty reader, sent by injected shell integration. vte drops OSC 7.
  • 2026-10-14: Git through the git command line, not libgit2.
  • 2026-10-14: license MIT, as Ely.
  • 2026-10-14: on Unix the pty is ELYTerminal's own: alacritty 0.26 ignores chdir's result, so a session could start in the wrong folder. Its traits feed alacritty's event loop, keyed 0 and 1 as alacritty's private constants are; alacritty is pinned to =0.26.0 for that.
  • 2026-10-14: bash integration loads through --rcfile, not Ghostty's --posix and ENV: macOS's bash 3.2 ignores ENV there. bash then runs as a non-login shell; on macOS the script reads the login files itself, so logout and ~/.bash_logout do not apply.
  • 2026-10-14: icons fall back by hand: a private-use character the font lacks paints in JetBrains Mono NF Mono. gpui's macOS fallbacks go through CoreText's cascade list, which never sees fonts added in memory, and gpui loads no font without an m, so the symbols-only Nerd Font cannot load.
  • 2026-10-14: box drawing (U+2500–U+257F), block elements (U+2580–U+259F) and powerline shapes (U+E0B0–U+E0B7) follow Ghostty's sprite rules (MIT, LICENSE-GHOSTTY): whole device pixels, line thickness the underline's, each cell clipped to itself, so lines join across cells whatever the font. A character's pieces never overlap: gpui blends every quad and every path triangle on its own, so a dim line painted twice brightens where it crosses. A drawn base keeps its geometry under combining marks, which paint over a no-break space.
  • 2026-10-14: keys follow Ghostty's encoder (MIT). Keys the program reads go out from the key listener; plain text falls through to the input handler, so IME and dead keys compose. Ctrl+[, Ctrl+I and Ctrl+M go as fixterms CSI u, as in Ghostty. Keypad keys always type their characters: Ghostty keeps mode 1035 on, and gpui names keypad keys as main keys on every platform. Ely binds Tab, Shift-Tab, F6 and Shift-F6 to focus moves, and bindings run before listeners; NoAction bindings under the Terminal context hide them, so these keys reach the listeners with press, repeat and release.
  • 2026-10-14: each kitty flag reshapes only what it names, as kitty's encoder does: functional keys keep legacy bytes until disambiguate, events or all is set, and text keys type until all is set. Cmd is kitty's Super, bit 8; a Cmd combo no binding takes reaches the program.
  • 2026-10-14: an input method's commit goes to the program as UTF-8 under every kitty flag, as kitty (keys.c, GLFW_IME_COMMIT_TEXT) and Ghostty (test kitty: composed text with report all) send it.
  • 2026-10-14: what gpui hides from the kitty protocol: a key alone on a modifier sends nothing (gpui reports no key event for it). gpui names a key Shift turns into a symbol by that symbol and clears Shift; on macOS and Windows the layout gives the key back, as below, and on Linux the symbol goes as its key, without Shift.
  • 2026-10-14: on macOS, which Option key is down and whether a key repeats come from AppKit's current event: gpui rebuilds keys AppKit routes through its commands as fresh presses. As in Ghostty, ApplePressAndHoldEnabled is registered off, so held keys repeat instead of opening the accent menu, and a local event monitor hands Command key-ups to the focused view, since AppKit never routes them to the window.
  • 2026-10-14: mouse reports follow Ghostty's rules: X10 tracking sends presses alone without modifiers, SGR names the released button, motion inside one cell sends nothing, Shift takes the mouse back for selecting. The tap follows modes 9, 1000, 1002, 1003, 1005 and 1006 as xterm holds them: one tracking and one format at a time, the last set wins, any reset ends it, and RIS clears both. alacritty drops 9 and keeps each mode apart, so its mouse bits go unread. The tap's scanner reads controls inside a sequence as vte does: C0 runs, DEL and high bytes are skipped, CAN and SUB cancel, ESC restarts, subparameters are skipped. Motion reports the first held button: left, right, middle. The pointer over the grid is the arrow, as the owner asked, not Ghostty's I-beam.
  • 2026-10-14: a wheel notch scrolls 3 lines, as Ghostty's discrete wheel; a trackpad scrolls its pixels. The wheel reports to a program that tracks the mouse, sends arrows on the alternate screen under mode 1007, and scrolls history otherwise.
  • 2026-10-14: Cmd-C and Cmd-V copy and paste on macOS, Ctrl-Shift-C and Ctrl-Shift-V elsewhere; on Windows Ctrl-V pastes too, as in Windows Terminal, since Windows shells take it so, while on Linux it stays the program's, as in GNOME Terminal and Ghostty; Cmd-press, Ctrl-press off macOS, opens an OSC 8 target or a URL Ely's links finds in the wrapped line. Copies drop the newline alacritty ends a whole line with, as Ghostty's do. Bracketed pastes lose ESC and Ctrl-C, as alacritty's do; plain pastes end lines with CR. Typing and pasting clear the selection. Copy on select and OSC 52 writes to p or s go to the primary selection on Linux, as Ghostty's do; macOS and Windows have none, so they go to the clipboard.
  • 2026-10-14: a key's release goes out only when the terminal saw its press, so a key a binding takes sends no release, as in kitty.
  • 2026-10-14: find opens on Cmd-F (Ctrl-Shift-F off macOS) beside the grid, outside the Terminal key context, so its keys never reach the program. Matches wash in Ely's warning color, the current one stronger; the newest is current first, Enter steps older and Shift-Enter newer. Shift-Enter binds under TerminalFind > ElyInput, the context of Ely's text input, whose own Shift-Enter binding takes the key. Find reads the grid as it shows: combining marks ride their cells, hidden cells are blank, wrapped rows join, words are Unicode; alacritty's own search reads no marks and knows only ASCII word edges. New output, a resize and a settings change search again; the current match stays the same occurrence: its line kept, grown or cut at its end, held under the same line above or in its place, before such a line elsewhere, nearest where scrolling moved it; else the nearest by line, then column. Searching and following take one pass over the text and the matches.
  • 2026-10-14: the program hears focus in and out under mode 1004 when the terminal's focus or the window's activation changes. The window wears the program's title, the app's name while it names none. A bell is logged and makes no sound. The title comes from the tap, in the output's order, read as vte and alacritty read it: OSC 0 and 2 set it, 15 parts past the number at most, since vte keeps 16 parameters, and 65536 bytes at most, past which it drops, logged; CSI 22 t and 23 t save it and bring it back; a full reset, ESC c, empties it. alacritty's own title events come after the tap's, and send nothing at a full reset, so they pass by.
  • 2026-10-14: the title bar is transparent, as Ghostty's: the content runs under it, Ely's column headers drag the window, and the terminal's header shows the program's title. macOS draws its traffic lights at Ely's origin; elsewhere Ely draws the controls, so windows ask for client decorations, which only Linux reads, and Ely's ResizeBorder gives the edges. Cmd-B and Cmd-Alt-B (Ctrl-Shift-B and Ctrl-Shift-Alt-B off macOS) toggle the sessions and right columns, kept in the settings. A button at each end of the title bar does the same: PanelLeft leads the first column, right after the traffic lights, and PanelRight ends the last, before the window buttons Ely draws on Windows and Linux, so neither moves when a column hides; its tooltip says Hide or Show. On Windows and Linux, which have no menu bar, a gear just inside PanelRight opens Settings, as Ctrl-, does. It sends OpenSettings through Window::dispatch_action, which waits for the click to finish: App::dispatch_action updates the active window at once, which fails inside that window's own handler, and only gpui's log says so. The Git and Files switch tops the right column, centered below the title bar: on Windows the caption buttons left the last header no room for it.
  • 2026-10-14: session keys are Ghostty's: Cmd-T, Cmd-W, Cmd-1 to Cmd-8, Cmd-9 for the last, Ctrl-Tab and Ctrl-Shift-Tab; off macOS Ctrl-Shift-T, Ctrl-Shift-W and Alt with the digits. A digit past the last session goes on to the program. A new session runs the Shell setting in home or in the shown session's folder, as Start in says. A session whose program ends closes its row; the last row closes the window, and with no window left the app quits. A bell in a hidden session marks its row until it is shown. A hidden terminal hears no mouse release, so hiding it ends its mouse gesture as releases would: the selection drag ends, and the program hears each held button come up. A row's title is the program's, or the app's name while it names none.
  • 2026-10-14: Files shows the shown session's folder in Ely's Tree, not FileTree, which takes every path up front and drops empty folders. A folder is read off the main thread when it first opens; the root and the open folders are read again whenever the session reports a folder, so at each prompt. Git's marks come from git status --porcelain=v2 -z -- ., run with --no-optional-locks, so it never holds index.lock against the user's own git, and under the user's status.showUntrackedFiles, so a home folder kept in git stays quick. An untracked folder marks what it holds. On macOS, whose file systems ignore Unicode forms, a name keeps its form on disk and meets Git's mark in composed form, as git's core.precomposeunicode writes it. A label built from a path shows each control character as its picture, ␊ for a newline: gpui shapes no newline, and a file may be named with one. A name not in UTF-8 keeps its bytes: its label shows it lossily, and its tree key adds the bytes after a NUL, which no path holds, so no two names share a key. A program without shell integration gets a line in the right column until it reports a folder.
  • 2026-10-14: git runs in English with --literal-pathspecs and without optional locks. Paths stay git's bytes until shown, so a name not in UTF-8 still names its own file; off Unix git writes UTF-8. Status reads porcelain v2. Paths to stage and unstage go in on stdin, NUL-ended (--pathspec-from-file), so any name and any count pass; an empty list is refused, since git would take it as the whole tree. Unstaging is git reset, which works before the first commit, and a staged rename unstages with its old path; a copy is a new file, its source no part of it. A diff is git's own, without external tools or text converters, a conflict against HEAD, our side, so a file we deleted reads as new; it is read into Ely's stretches; a binary file has none, and an untracked folder none either. Without an index git exits 1 for a difference and a failure alike; only a difference prints. What git warns of while it works goes to the log. The log needs a commit at HEAD: git refuses it before the first.
  • 2026-10-14: the Git panel shows HEAD's branch, or its short commit while detached; Ely's ChangesList of conflicted, staged, unstaged and untracked files; Ely's CommitInput; Ely's CommitList of the last 50 commits. A row's press opens its diff in Ely's fullscreen Dialog under the title bar, in Ely's DiffViewer, unified or split; a binary file, an untracked folder and a change of no line say so instead. Every control Ely draws works: stage, unstage, stage all, unstage all, amend, and discard, which asks first in Ely's ConfirmDialog: an edit goes back to the index (git restore); a new file, untracked or only intended to be added, is deleted (git clean, git rm -f). Git's clean passes over a repository of its own without a word, so a clean that removed nothing is an error. Actions run off the main thread, each after the one asked before it; a failure shows in Ely's Alert with git's words until dismissed or the next action, and after any action Files reads its marks again. A folder change drops the open diff, and one still loading, and blanks the view unless the new folder lies inside the shown work tree, so a cd within it does not blink; no action runs until the shown folder has read, so none reaches the repository left behind. A commit empties only the fields that still hold what it committed, and only once the shown folder has read and is in the same repository: a draft typed while it ran, or while a folder read, stays. A path shows on one line and once: as it is, or quoted as git quotes a name with control characters, quotes, backslashes or bytes not in UTF-8; a folder drops git's trailing slash, since no file shares its name. A section lists 500 files at most, and a line says how many are listed: Ely's list draws every row, and gpui copies every row's hitboxes and paint each frame, so 3000 rows halved the terminal's frames. The Git body is a cached view, so a terminal's output does not draw it again; it comes back from the Files tab with its lists scrolled to the top, since a view cached while hidden drops its widgets' state. Files stays uncached, so its tree keeps its open folders: Ely's Tree draws only the rows in view.
  • 2026-10-14: Cmd-, opens the settings window, or brings the open one forward, and Cmd-W closes it; off macOS Ctrl-, and Ctrl-Shift-W, as Ghostty's. Categories sit in the first of two columns of Ely's ColumnShell, an Ely SelectableList as SettingsLayout draws its own, and the open one's rows, Ely's SettingsRow, in the second; each column scrolls on its own, which SettingsLayout, one box for both, cannot. A category joins the list with its rows. Each change goes through SettingsStore::update, which checks and saves, and every view reads the global, so it applies at once. A shell saves only when it names a program the running user may run, as the system's access answers, ACLs too: empty for the login shell, a file at a full path, or a name found in one of PATH's full folders; a relative folder counts for none, since it would hang on each session's own folder. On Windows a name without an extension means its .exe, as CreateProcess reads it; a name with one keeps it. A program whose path is not UTF-8 counts for none: a session starts its program by text. A session runs that same full path, so the check and the start agree; else the row says why and the saved one stays, since a bad shell stops the next start. The window takes focus inside when it opens, so its keys find its context. The app quits with its last terminal window: settings alone hold no terminal.
  • 2026-10-14: settings refuse unknown fields; a bad settings file stops the start and is never overwritten.
  • 2026-10-14: Appearance shows every built-in theme as Ely's RadioCard in a SimpleGrid, each over a small window in the theme's colors. Separate dark theme copies the theme into dark_theme, so turning it on changes nothing until a dark card is picked. A theme change re-dresses the chrome at once. Font family comes from Ely's FontPicker; size, line height and padding from its NumberInput, which saves a number on Enter, on leaving the field and on each step. A font saves, and a settings file starts, only when gpui loads it as the terminal draws it: gpui falls back to another font without a word and loads none without an m, so the terminal draws with no font but its own. The cursor preview is a prompt in the terminal's font and theme, its cursor drawn as the terminal draws it, blinking on the terminal's beat. Opacity moves by whole percents, so the saved fraction carries no float noise. A change the store refuses leaves the setting as it was, and the page says why at its top; a number field drops its draft, and the font picker shows the saved family again. The settings window opens at 900 by 640, room for three theme columns.
  • 2026-10-14: Terminal shows the shell and where sessions start, which a General page held until the owner dropped it, then scrollback lines, Option as Alt, on macOS only, since elsewhere Alt is Alt, copy on select, and Ely, then every shortcut in Ely's KeyboardShortcutsList, read from the keymap: the bindings of each listed command that apply in the main window's terminal, matched at any depth of its context stack as gpui matches them, so the list cannot drift from the keys. The engine reads each when it needs it, so a change applies at once to every session; a smaller scrollback drops the history past it.
  • 2026-10-14: the main window lets the desktop through while Background opacity is under 1, blurred or clear as Background blur says, through gpui's Blurred (macOS's NSVisualEffectView, Windows' acrylic) or Transparent; at 1 it is Opaque. The workspace sets it as the settings change. Ely's ColumnShell::glass, added to Ely for this, grounds the columns in the palette's glass, which the opacity sets, so the title bar and every column let the desktop through alike; the palette's bg stays solid, so a dialog, the Git diff among them, hides what lies under it. The terminal paints no ground over the glass, or the two would stack; a program's own background (OSC 11) paints over it. Ely's own themes pass their palette too, Ely's colors with the glass set. The settings window stays opaque.
  • 2026-10-15: on Wayland the window asks for ext_background_effect_v1 beside gpui's org_kde_kwin_blur. A guest connection on gpui's own display binds it, takes the effect for the window's surface once, and blurs the window's pane: the surface less Ely's resize margin on each free edge of a client-drawn window, worked out each frame and sent only when it changes; it goes and comes back with Blurred and lands at the surface's next commit, which every frame makes. The guest connection reads the display only while the window opens, before gpui's loop runs, and is never dropped: a drop destroys its proxies through the display, which gpui may close first. The log names what the compositor offers, so which way blur goes: ext_background_effect_v1, gpui's org_kde_kwin_blur, or none, the desktop clear. Its test runs a wayland-server compositor on a socket pair.
  • 2026-10-15: on a client-drawn window, so on Linux, Ely's ResizeBorder paints its pane solid and shadowed under the content, which kept the desktop from showing at all. ResizeBorder::glass, added to Ely for this, leaves the pane to the content while the window is see-through: no ground, nor the shadow gpui draws under the box, which would tint it. So the blur covers the pane alone and the resize margin stays clear. On X11 the window names its pane in _KDE_NET_WM_BLUR_BEHIND_REGION (CARDINAL, 32: x, y, width, height in the window's pixels), as Ghostty names its own, and drops the property when Blurred goes; KWin reads it, other compositors pass it by. It goes over a connection of ELYTerminal's own to $DISPLAY, which gpui opens too: a property belongs to the window, whoever sets it. gpui's own org_kde_kwin_blur on KDE's Wayland still covers the whole surface, margin included, beyond ELYTerminal's reach.
  • 2026-10-15: the macOS menu bar is the PRD's: ELYTerminal (Settings…, Quit), Shell (New Session, Close Session), Edit (Copy, Paste, Select All, Find), View (Toggle Sessions, Toggle Right Panel, Bigger, Smaller, Reset Size), Window (Minimize, Zoom, then the windows macOS lists). An item shows the key bound to its action, so the menus are set after the bindings; Copy, Paste and Select All go through AppKit's copy:, paste: and selectAll:, as gpui's OsAction maps them. They are Ely's text input actions, which forms exports for this, and the terminal handles them too: gpui sends a menu's action to what holds focus and enables the item only where a handler waits, so one action serves the terminal, find and every field. The new keys are Ghostty's: Cmd-Q, Cmd-A, Cmd-= and Cmd-+, Cmd-minus, Cmd-0, Cmd-M. Off macOS Quit and Select All take Ctrl-Shift, since Ctrl with a letter types a control byte; the font's size takes plain Ctrl with =, +, minus and 0, which type none, as in Ghostty, Alacritty and Windows Terminal; Minimize has no key there. Bigger and Smaller move every session's font a point, inside the Size range, and Reset Size goes back to the setting; nothing saves the change, as in Ghostty. Select All runs from the first character in the history to the last on the screen, the blanks around them left out, as Ghostty's, though a space under a combining mark shows a character and counts as one; copy on select takes it, and a blank grid selects nothing.
  • 2026-10-15: CI runs on self-hosted Gitea runners, each job with what its runner must hold. macOS runs on the host, with fish and PowerShell installed, or the two shell tests fail as they should. Windows runs on the host, with rustup's cargo and rustc proxies beside rustup and Visual Studio's C++ tools. Ubuntu (ubuntu-24.04) runs as root in docker.gitea.com/runner-images:ubuntu-24.04, installs the libraries gpui links, and runs the tests as a user it makes, since root ignores the file modes they check; its runner serves this repository alone, in a VM of its own, so a build cannot starve other containers. The Linux and Windows jobs build without debuginfo, as scripts/linux.sh does: with it, linking gpui's test binaries outgrew that VM's 8 GB. The macOS and Windows jobs run on the owner's own machines, so they skip Swatinem/rust-cache: its post step cleans $CARGO_HOME, which there is the owner's own, and after every job it deleted rustup from ~/.cargo/bin, leaving its proxies pointing at nothing, and pruned the shared registry; the Linux job keeps it, inside its container. A run that waits on a missing runner stays queued: cancel stale ones before a new runner registers, or it runs every one. The shell tests type a line only once the line editor reads raw keys, canonical mode off: on CI, a line typed before PowerShell's first read was echoed by the terminal and never run. Every job installs the toolchain rust-toolchain.toml names with rustup toolchain install --no-self-update and never runs rustup default, so the owner's machines keep their own default toolchain and rustup; the macOS job puts the host's ~/.cargo/bin on the runner's PATH, and the Linux container installs rustup first. A toolchain file's components install only with the toolchain: one already there without them makes each cargo call try to download them, so scripts/linux.sh and scripts/linux-tar.sh add them to their images.
  • 2026-10-15: on macOS 27 gpui's Blurred gives its NSVisualEffectView the Selection material, which builds no backdrop layer, so the desktop showed through sharp. blur/macos sets the view to underWindowBackground whenever the window turns Blurred: a gaussian blur of radius 30, the radius the owner's Ghostty uses; gpui's updateLayer still strips the material's tint and saturation. The view's presence proved nothing, so shot reads the radius off its backdrop layer.
  • 2026-10-15: the icon is drawn by hand in SVG, flat, so one figure serves both forms: on macOS it stands on a night-purple rounded square on Apple's grid (824 of 1024, radius 185) with sparkles, the gradient grained so it does not band; on Windows and Linux it stands alone, as Papirus draws app icons. The app is com.zacharyzhang.elyterminal, after the owner's domain. A local build is signed ad hoc: it opens on the Mac that built it, and Gatekeeper asks once on any other; a release's is signed with the owner's Developer ID and notarized, so it opens on any Mac.
  • 2026-10-15: a release is a Gitea release tagged vVERSION with three builds, each from its script: the macOS disk image for Apple silicon, a Windows zip and a Linux tarball for x86_64. Each window names itself com.zacharyzhang.elyterminal through Window::set_app_id as it opens: on Linux that is Wayland's app id and X11's WM_CLASS, which the desktop entry's name and StartupWMClass match, so a dock pairs the window with the icon; gpui's X11 windows read no app id from WindowOptions.
  • 2026-10-15: on Windows a release build is a GUI program, so no console window opens beside it; a debug build keeps its console for the log. git then starts with CREATE_NO_WINDOW, or each call would open a console. Windows builds link the C runtime statically, so the executable needs no Visual C++ redistributable. gpui's build script embeds its manifest, PerMonitorV2 DPI awareness among it; build.rs adds the icon as resource 1, which gpui loads for each window, and stops the build when no resource compiler is found.
  • 2026-10-15: the Linux tarball builds on Debian 12, in rust:1.95.0-bookworm: a binary asks for the glibc symbol versions it was linked against, so it is linked against an old glibc, not the builder's. scripts/linux-tar.sh prints the glibc its build asks for, and the README names it: 2.35 for v0.1.0, which runs on Ubuntu 22.04.
  • 2026-10-15: on Windows gpui answers the system's hit test from the last frame: under the pointer, a WindowControlArea::Drag hitbox that nothing occludes makes it the caption, and Windows keeps the press, so no click reaches the app. In v0.1.0 every title bar control was dead so. Ely now occludes the drag region under its window buttons, which then answer as close, maximize and minimize, and under each column header's item row, whose controls get plain presses; the row stays the scroll container, and its title is a drag region of its own.
  • 2026-10-15: a release's macOS disk image comes from the Release workflow, dispatched once the draft holds the Windows and Linux builds. It runs on macos-signing, the Mac mini's runner in the owner's login session, whose keychain holds the Developer ID identity; at the owner's choice that runner moved from AgentCompany alone to the owner's level, so every repository reaches it. The job reads the App Store Connect key from the owner's user-level secrets APPLE_API_ISSUER, APPLE_API_KEY_ID and APPLE_API_KEY_P8, writes the .p8 into the runner's temp folder for notarytool alone, and puts the stapled image on the draft through scripts/release-asset.sh with the job's own token. One notarization covers the image and the app inside it; the image carries the ticket, and the app copied out finds its own online on its first open.
  • 2026-10-15: on Omarchy 4 (Linux) ELYTerminal follows the system's theme. Omarchy renders the current theme's colors.toml through its own resolver into ~/.local/state/omarchy/current/theme/ghostty.conf, under $HOME whatever XDG_STATE_HOME says, so ELYTerminal reads that file and wears what Ghostty wears there: its color lines go through the strict parser the bundled themes use, and the rest, such as the glass block, pass by. Omarchy writes no cursor-text, so the background stands in, as in Ghostty and Omarchy's kitty template. ELYTerminal reads the file twice a second, since Omarchy signals only Alacritty, kitty and Ghostty, by name, and wears it whenever its text changes and parses; a check that fails is logged once while it lasts and tried again, so a file made readable again is worn though nothing else about it changed. The Omarchy theme is the default unless that file is surely absent, so a folder that cannot be read makes a fresh install wear it and the start name the error; another pick stops following. At start a missing file means no Omarchy unless the settings wear it, a file that cannot be read stops the start, and one that does not parse stops it only when worn, else it is logged and no Omarchy card shows. While running, a missing file is a switch in progress, and any other failure keeps the last theme. Omarchy 3's ~/.config/omarchy/current gets no shim.
  • 2026-10-16: on Hyprland 0.56 a window blurs only while Hyprland's own decoration:blur:enabled is on: its shouldBlur reads that option first, then the window's no_blur, RGBX and opaque rules, then the blur region a surface sets through ext_background_effect_v1, which Hyprland offers with the blur capability whatever the option says. Omarchy 4.0.4's default looknfeel.lua turns the option off, so on Omarchy no window blurs until the user turns it on; ELYTerminal leaves the compositor's settings alone. On the owner's Omarchy the log named ext_background_effect_v1 and the pane's region, and the wallpaper behind the window blurred with the option on and showed sharp with it off.
  • 2026-10-16: herdr 0.9 keeps its panes' OSC 7 and writes only a title, notifications, OSC 52 and bells to the terminal it runs in, so no terminal hears a folder from inside it; herdr's server knows each pane's cwd, the folder its shell reported, else the shell's own. On macOS and Linux each session asks twice a second which process group leads its pty's foreground (tcgetpgrp on a copy of the master) and what executable that group's leader runs: an exec keeps the group and swaps the executable, so exec herdr from a shell is a new program, and one that did not read is tried again; on Linux the (deleted) mark an update leaves on a running executable is set aside. When that executable is herdr and its arguments start herdr's terminal client (no command, session attach NAME or client), ELYTerminal reads the client's arguments and variables (/proc on Linux; KERN_PROCARGS2 on macOS, which hides only the variables of Apple's own programs) and finds its socket as herdr 0.9 does: --session NAME, then HERDR_SOCKET_PATH, then HERDR_SESSION, under $XDG_CONFIG_HOME/herdr, else ~/.config/herdr; a relative socket lies in the client's folder, as herdr reads it (/proc/PID/cwd; proc_pidinfo on macOS). It asks that socket for session.snapshot twice a second, a connection each, since herdr closes one after its reply, and reports the focused pane's cwd as the session's folder when it changes and again each time herdr takes the foreground; an answer that lands after the shell reported a folder is dropped, since the shell reports only once herdr has left. Focus belongs to herdr's server, so clients of one server follow one focus; a client of another machine's server (--remote, --machine) is logged, not followed. A failure is logged once while it lasts. On Windows, which has no foreground group, a Toolhelp32 snapshot lists the herdr.exe processes at or under the session's shell, none under another herdr, since what a herdr starts, its server too, is its own; when that list changes, sysinfo reads their arguments and variables, nearest first, names in upper case, as Windows matches them, and the first terminal client is followed, a server or a command beside it passed by; its folder is found as herdr 0.9 finds it there, XDG_CONFIG_HOME, then APPDATA, then USERPROFILE\AppData\Roaming, then HOME\.config; and its server answers on the pipe herdr names \\.\pipe\ and the socket's path as written, relative or not. A busy pipe is waited for, as herdr's client waits, and the pipe's server may identify ELYTerminal but never act as it. The pipe has no timeout: a herdr that never answers holds that session's follow until it answers or exits.
  • 2026-10-16: the main window opens at the size it was left at, which the owner asked for on macOS, Windows and Linux: window_width and window_height in the settings hold the size gpui's window_bounds gives 0.5 s after the bounds settle, saved through SettingsStore::update, which writes only a change, so a drag saves once and a move saves nothing. A maximized, full-screen or hidden window keeps the size from before; its state and size are read once they settle, since X11 marks a window maximized after its new size, gpui's X11 window then gives a maximized window's own size, and it calls a maximized window unmaximized while minimized. A window that shows again, uncovered, restored or back on its screen, is read again, so a size it took just before it hid is kept; a size it takes in the half second before it closes, or before it hides until the app quits, is lost. The window is no smaller than 100 by 100, the least the settings keep, or a smaller size the platform allows could not be kept. The window opens centered at whole points, since macOS grows a window by a point on a side whose edge falls on a half point; its place is not kept, and a kept side past the primary display's visible area shrinks to it, logged. A size outside 100 to 16384 stops the start, as any value out of range does.
  • 2026-10-16: the sessions and right columns resize by a drag on their border toward the terminal, which the owner asked for on macOS, Windows and Linux. Ely's ShellColumn::resizable, added to Ely for this, puts Ely's resize_handle on the border toward the filling column, the sessions column's right and the right column's left: the handle draws that border, a filled hairline, since gpui paints a border no wider than half its box, and the line it drew at first showed half a pixel on macOS and none on X11; it occludes the header under it, so a press there drags the border, not the window, on Windows too, where an unoccluded drag region takes the press as the caption. A drag sets off from the wider of the width drawn and the one kept: a header's parts may hold a column wider, and a window too narrow for both widths squeezes them, where a drag from the squeezed width jumped back against the pointer; a drag that has not moved sideways changes nothing, and one that comes back where it set off gives the width it had. A set width in Ely became a flex basis, so a column holds its header's own parts: at a set width under them, Windows' caption buttons ran out of the window. The widths, 160 to 960 pixels, show at once and are saved in sessions_width and panel_width through SettingsStore::update 0.5 s after a drag rests, as the window's size is; a drag in the half second before the window closes is lost, and a save the store refuses puts the columns back. A window too narrow for both yields the terminal its header's width, as Ely's set widths give way.
  • 2026-10-16: prompts and commands carry OSC 133 marks, which the owner asked for, read as Ghostty reads them: A or N starts a prompt, B or I ends it, C starts a command's output, and D ends the command with its status, D's first option, none when that is no number. The tap reads any program's marks before the parser, so it never sees the cursor: A's fresh line and L, which need it, are not done, and P adds nothing to A. Each session keeps the status its last command ended with. The integration marks what the shell leaves unmarked, and the user's prompt still sees the command's status. zsh: precmd reads $? and sends D after a command ran, preexec sends C, and a last precmd hook wraps PS1 in A and B under prompt_percent, which the first prompt says while it is off; it moves to the end of precmd_functions at each prompt, since a theme that sets PS1 in a hook of its own loads after it, and at the first prompt sched +0 wraps it once more, since zsh runs due events after every precmd hook; that call returns the command's status, which the prompt shows, but it would reset $pipestatus, so it serves the first prompt alone. bash: PROMPT_COMMAND's first entry reads $?, which each entry resets, and its last wraps PS1. From bash 4.4 PS0 sends C and its subscript notes that a command ran, since bash prints no PS0 for an empty line, and the first entry sends D for it; both need promptvars, on by default. macOS's bash 3.2 has no PS0 and nothing else between reading a line and running it: its DEBUG trap runs for a key binding's command (bind -x) as for a typed one, with nothing to tell the two apart, runs for no line that is only a subshell, and under extdebug its status decides whether a command runs. So bash 3.2 sends no C, and Ghostty's automatic integration skips bash 3.2. Its D comes from PS1 once \#, the command number, has moved, which neither a key binding nor an empty line does; readline sends a prompt's escapes again at each redraw, so that D goes again with it, the same status. Every mark of a command rides an expansion, which promptvars turns on, so while it is off at start the script says so rather than mark nothing without a word. fish 4.3 marks its own prompts and commands, fish 4.0 all but B, older ones none: the script adds what the running fish lacks, around the prompt shown, and wraps it again when the user's changes. PowerShell: the prompt reads $? first and sets it back for the user's prompt through Write-Error '' -ErrorAction Ignore, which adds nothing to $Error. A failure ends with $LASTEXITCODE when a program set it during the line, since each run stores a new object, equal or not, which [object]::ReferenceEquals tells apart, unless the line surely ends in PowerShell: its syntax tree, read as PSReadLine hands the line over, ends in a cmdlet, as Get-Command resolves the name, aliases through, in a name not found, or in no command; such a line ends with 1, a cmdlet's failure under -ErrorAction Ignore too. A line that ends in a block, a function, a script or a call through a variable takes the code of a program it ran: only a hook on every command lookup would name the last command run, and it slowed a loop of commands by half. $LASTEXITCODE is read through Get-Variable, since strict mode refuses an unset variable and a prompt that fails falls back to PS>. C goes out when PSReadLine hands over a line that is not blank; without PSReadLine the script says it marks no command. herdr passes no OSC 133 on, so a session running herdr keeps the status from before it.
  • 2026-10-16: the view jumps between prompts, which the owner asked for, as Ghostty jumps: Cmd-Up and Cmd-Down, with Shift or without, on macOS, Ctrl-Shift-Up and Ctrl-Shift-Down elsewhere, scroll to the prompt that starts before or after the view's top; a prompt on the screen's last page means the bottom, which the view cannot pass, and with none there nothing moves. alacritty's rows hold no marks, and the tap, which reads before the parser, never sees the cursor, so the tap rewrites what goes to the parser: an OSC that may be a mark is held whole until it ends and read as marks reads it, and a prompt's start becomes an OSC 8 link of the terminal's own, its target \x7f, which no program names, its id \x7f and the prompt's number; the prompt's end closes it. The parser stamps each cell the prompt prints with the link, SGR 0 keeps it, and it rides the cells through scroll, reflow and history; a prompt starts on the first row that shows its number, blank rows within it passed. A program's own link keeps its target and takes the prompt's number in its id: one opened inside a prompt carries it, and a prompt that starts inside one opens it again with the number and gives back its own id at the prompt's end, its target kept whole as the scanner keeps it; its end inside a prompt takes the prompt's link up again. A cursor restore that brings back a link passes the rewrite by. An OSC held, a body of 4096 bytes at most as the scanner keeps it and twice that with the controls vte skips, shows nothing until it ends; controls vte runs inside an escape go on before an ESC that waits, and only an ESC from ground waits: one that ends an OSC goes on with it, so what follows runs where the parser stands. A rewrite turns a mark of 8 bytes inside a program's link into that link again, its target up to 4096 bytes, so the tap reads about 2 KB of the buffer's room at a time, which moved 3 MB through a pty as fast as larger reads. Links, Cmd-press among them, pass over the prompt's link. The tap's OSC scanner ends a body as vte does, at BEL, ESC, CAN or SUB, and skips the controls vte skips, so a mark and its link agree.
  • 2026-10-17: programs say what they are doing through OSC 7501, revision 0.4 of the Program Status Protocol, which the owner asked for, read as the spec reads it: pairs key=value joined by :, a malformed pair skipped, an unknown key passed by, and every pair of a known key checked, a clear's too, before the last of each key is kept. A sequence over 4096 bytes, counted from OSC through ST with the controls its body skips, a key over 16, an app over 32, an id outside its grammar or limits (128 bytes, 32 a segment, 8 levels), a title or msg over its limits, not base64, not UTF-8 or holding a control character, and a missing or unknown state each drop it whole; kind counts only with blocked, and progress, 0 to 100, only with working and blocked, each absent when unknown. Each terminal keeps its records by id, the root's empty: a report replaces its record whole, clear drops the record and those under it, or all without an id, and past 256 the record updated longest ago goes. A new prompt drops working, blocked and idle records, as kitty does, and keeps done and error; a full reset, which the tap's scanner sees as ESC c, through the controls, DEL and high bytes vte passes by in an escape, drops all, and DECSTR nothing. The query OSC 7501 ; ? is answered with every state and kind; the view writes the answer when the session hears the query, ahead of the parser's answers to what came later in the same read, so a DA1 sent after the query, as the spec suggests, comes back second, and one sent before it in the same read comes back after it.
  • 2026-10-17: programs ask for notifications through OSC 9, 777 and 99 and report progress through OSC 9;4, which the owner asked for. OSC 9 is ConEmu's command where Ghostty reads one, 9;1 to 9;12 each by its own shape, else iTerm2's notification, its text the title; OSC 777 is rxvt's notify;title;body, another extension logged and passed by. OSC 99 is kitty's: k=v pairs before the payload, a malformed pair passed by; i, of [A-Za-z0-9_-+.] alone or the sequence drops, gathers chunks while d=0, 32 notifications and 16384 bytes of text at most; a payload holds 2048 bytes plain or 4096 encoded at most; plain text is UTF-8 without a control character; e=1 reads it as base64 group by group, so chunks split before encoding or after join, and it carries any UTF-8, newlines too, checked once the text is whole; a chunk that breaks a rule drops what its notification gathered; metadata holds 1024 bytes at most, and the tap keeps an OSC 99 body to 5124 bytes, room for metadata and a full payload, where other bodies stop at 4096; p takes title, body, close, ? and alive, and other payloads pass by; a, c and w are kept for posting, and o passes by; a lone body is the title, and one with neither passes by. The query and alive are answered as the notifications below say; an answer names the asker's id, or 0. OSC 9;4's states, remove, set, error, indeterminate and pause, read as Ghostty reads them, the progress clamped to 100, stand as the root record: set and pause working with their progress, indeterminate working without, error an error, remove none; this lasts until a program reports through OSC 7501, and again after a full reset, as the spec asks, so a mapped 9;4 never wipes a real report. The terminal posts the notifications, as below.
  • 2026-10-17: inside herdr, which passes no OSC 7501 on, herdr's agents are the session's records. The snapshot the follower asks for lists them; one in a state herdr knows, idle, working, blocked or done, is the record herdr:PANE, whose colon no OSC 7501 id holds, so a program's record of a name like it, herdr/build among them, stays its own; unknown and any state herdr adds later pass by. herdr names no kind and no progress; the app is the agent, the title its name, else the name herdr shows for it, and the message its terminal title less the spinner herdr strips, else the title its metadata sets; herdr's done is idle and not yet seen. A snapshot that lists other agents replaces them whole, so a full reset or a clear that dropped them sees them back at the next; they go when herdr leaves the foreground, and a snapshot that lands after the shell reported a folder is dropped, its agents with it. A change of their states is logged; a herdr whose snapshot lacks agents lists none.
  • 2026-10-17: a session's row shows its status, which the owner asked for: one mark before its title, the most pressing state first: 3 by 3 dots in the state's color, which the owner asked for, moving, after the S1 orb of aicss.dev; the dots and their motion are ELYTerminal's own. Working ripples out from the centre to the edges, then the corners, once every 1.2 s; with progress the outer ring fills clockwise from the top, as Ely's ProgressRing did, and the next dot pulses, the centre last. Blocked, in the theme's warning color, stays lit and dips twice every 1.6 s, whatever its kind, which hover names. Error, a program's or a failed last command's, draws an X in the danger color; done a check in the success color; idle a dimmer ring in the muted color. A settled shape lights dot by dot, then holds still: one that kept moving would draw the window for as long as it stands. The dots outside a shape stay faint. Every mark steps on one clock, every 80 ms, so the marks move together, and the window draws again at each step only while a mark it shows moves: a working program costs 12.5 frames a second, not one at every refresh of the display, which kept the loader still before. Every mark takes a 24-pixel box, so the title stays put as the state changes; the dots fill 16 of it, an icon's room. Among the records in that state, the one reported last speaks. Hover over the mark lists each record, ten at most: its state, its app, its own or its nearest ancestor's as the spec asks, its title and its message, each on one line and cut at 120 characters, with C1 controls, direction controls and invisible formatting taken out, as the spec asks; joiners stay, since scripts and emoji need them. A failed last command's status follows. Hover reads the records as they stand at each frame, so an open one follows a change; it stays while the pointer is over it and scrolls within the window's height, so ten long records all show. A key the user types, a paste too, sees the terminal's done and error records, which then go, as the spec suggests; herdr's stay, since herdr marks them seen. The view notes the state its row shows after each change it hears, not once an output, and tells the workspace of each, and a hidden session whose mark turns to blocked sets its title strong until it is shown, though it went on within the same output, so one output marks as two would. A change of herdr's records draws the view again, so an open hover follows them though the state holds. The view logs each change of that state, and keeps its last command's status.
  • 2026-10-17: a session posts system notifications through gpui's, which the owner asked for. At start set_app_identity names the app: on Windows that sets the process's AppUserModelID, which the taskbar groups its windows by too, so a pin made before may show apart, and the first notification writes the app's name under HKCU\Software\Classes\AppUserModelId, as gpui needs for an app without a package; on macOS gpui posts only from the app bundle, asks for leave at the first, and logs once, under the gpui_macos target, that it posts nothing outside one; on Linux it posts over D-Bus through notify-rust. Each terminal view posts its own, in the order its output asks, so OSC 99's answers keep their place before a DA1 sent after them: when its row's mark turns to waiting, done or error, at each turn, as the row's waiting mark is set, the hover's line for the record its body; when a command C started ends, Command finished, or Command exited with N, so a prompt drawn again, as macOS's bash 3.2 draws one with its D at a resize, posts nothing, and bash 3.2, which sends no C, posts no command's end, as Ghostty's integration, which skips it, posts none; and when a program asks through OSC 9, 777 or 99, its title and body the body, each line shown as text. It posts only while its window is not active or it is not the session shown, which the workspace tells it, one every 3 seconds at most, a later one logged and dropped; the title is the program's title, else the app's name, as the row names the session, which the tap reads in the output's order, so a title set before a notification in the same output names it. The session's own share a tag, so a new one takes the old one's place where the system allows, as an OSC 99 id does; OSC 9, 777 and OSC 99 without an id each take their own; a session keeps its last 64. A press goes to the window whose session posted it: it shows the window and the session, unless OSC 99 said a=-focus, and tells a program that asked a=report, OSC 99 ; i=ID ;; a press whose record went, a twin Linux stacked and pressed before or one past the 64 kept, still shows its session and tells no program. macOS brings the app forward at any press, a=-focus or not, and on Wayland gpui asks for the window with a serial the compositor likely refuses, so KWin and Mutter mark it as asking for attention instead. gpui hears of no close on any system, so c=1 hears p=close;untracked once posted, and alive lists the ids posted and not yet pressed, closed or expired. On macOS and Windows a close takes the notification back, and w over 0 takes it back once it runs out, its session gone or not, unless a later post holds its tag; gpui on Linux closes and replaces none, so there a close and w pass by, logged, a closed one stays alive, and an update posts beside the old one. o passes by: each notification posts only while unseen, which is kitty's invisible, so the query answers a=focus,report:o=invisible:p=title,body,close,?,alive:w=1, on Linux a=focus,report:o=invisible:p=title,body,?,alive, never with the c that close events would need. Windows' toasts are silent, as macOS's are; on Linux the notification daemon decides, and reads the body as markup, so its &, < and > go as entities.
  • 2026-10-18: herdr's shortcuts, which a user found dead, as kitty and Ghostty send them. herdr 0.9.5 asks for kitty's disambiguate, events and alternates on macOS and Linux, and takes cmd+shift+ctrl+[ as [ with Shift, Ctrl and Super, CSI 91;14u, the symbol as its alternate, CSI 91:123;14u, as Ghostty sends it; gpui named the key { and cleared Shift, so ELYTerminal sent CSI 123;13u, which herdr matches only to cmd+ctrl+{. Under kitty's flags a key Shift made a symbol of now goes as its own character with Shift, the symbol its alternate; legacy keys keep fixterms' symbol without Shift, as Ghostty's do. On macOS the key's own character comes from UCKeyTranslate on the event's key code, in the layout gpui reads: Command's while Command is down or while the plain one types no ASCII; AppKit's charactersByApplyingModifiers: ignores the layout for events built in-process. On Windows herdr asks for no kitty flags and reads the console's key records, and ConPTY asks every terminal for win32-input-mode, CSI ? 9001 h; so, as Windows Terminal does, each key that types no text goes as CSI Vk ; Sc ; Uc ; Kd ; Cs ; Rc _: the virtual key from gpui's name and the layout (VkKeyScanW), its scan code, what Windows types for it (ToUnicode, a dead key left as it is), down or up, and the modifier keys, locks and enhanced block from GetKeyState. kitty's flags go first, as in Windows Terminal; text stays text, so a repeat or a release goes as a record where its press did, a repeat that types after its modifiers came up too, and a release goes so only there; a full reset ends the mode, as Windows Terminal's does. A key that is down is known by its code, AppKit's key code on macOS and the virtual key on Windows, since gpui names a release by the modifiers left down: a release after Shift came up still goes out. Linux has only gpui's name. A console record holds no Windows key, so no Windows terminal passes herdr a cmd binding, and a keypad key goes as its main key, since gpui names it so. On Linux gpui keeps neither the key code nor its xkb state, and names a non-ASCII key by its place on a US keyboard, so a key's own character cannot be found: Shift with a symbol and Ctrl, Alt or Super goes as the symbol without Shift, which herdr matches to a binding on the symbol, ctrl+{.
  • 2026-10-18: inside herdr, herdr opens its panes' links, as the owner's Cmd-press and Ctrl-press ask. herdr 0.9.5 wraps a long URL at its pane's edge with no wrap flag and writes its panes' OSC 8 links to the terminal without their targets, so ELYTerminal's own finder opened a cut URL or nothing; herdr itself opens the link under a Ctrl-press, whole from its own pane, a wrapped URL or an OSC 8 target, and underlines it while Ctrl is held. So while herdr's client leads the session and tracks the mouse, every link press goes to herdr as the mouse report it is, and on macOS herdr hears Cmd as Ctrl, the wheel aside. herdr's rules come with it, by its design: while it takes the mouse it keeps its panes' OSC 8 targets from the terminal, so that it alone answers Ctrl-press, and it opens only http and https links; a file:// link inside herdr stays shut, since the text the terminal sees may be the label of another link. Shift with the press, which takes the mouse back from a program, is the way past herdr its docs name: the terminal then opens what it sees, a file:// link too. With herdr's mouse capture off, herdr writes its links' targets out and the terminal opens them itself. Elsewhere ELYTerminal opens the link itself, as before.
  • 2026-10-18: Ely peeks over the main window's top edge, which the owner asked for after the 0.1.3 film, her head and fingers drawn from the icon's own paths and cut at the edge she holds, in a transparent window of her own, 112 by 107 points with 16 above her for her hops, 140 in from the pane's left edge, less on a pane too narrow for that, so she holds its edge at any width, and the main window seems to hide the rest of her. Her window never takes the keys from the terminal: on macOS her panel becomes key only if needed, which gpui's view never asks; on Windows her window answers a press's activation with none, every other message passed on to gpui's own procedure; on X11 and Wayland a press moves no focus. gpui moves no window and places its popups only on Wayland, so each platform keeps her there its own way: on macOS gpui's popup panel is made the main window's child, at its level and in its Spaces, without a shadow, so AppKit moves it and hides it with the window; on Windows gpui's popup is owned by the main window and loses the topmost place gpui gives it, so it stays just above the main window and hides with it; gpui makes it an overlapped window, which Windows gives a caption and a frame, and opens it as large as the display when the middle of the box asked for lies off every display, as hers does over a window near the screen's top: in 0.1.4 a user found her at the bottom of a framed window the display's size, which took the clicks meant for the windows behind; so it becomes a bare popup, without caption, frame, corners or shadow, and the glue sets its size, from its own DPI, with its place on the pane's top edge as the main window moves, posting both to her window, since Windows sends a new size at once and gpui hears it only outside its own updates, as gpui defers its own resize; her height is her own DPI's and her bottom the pane's top, whatever the two windows' DPI, and when a move takes her to a display of another DPI, gpui takes the rect Windows suggests and she keeps her bottom left corner where it stood; on X11 gpui's popup, which no window manager places, is moved onto the pane, stacked just above the window manager's frame as the window moves or comes forward, and unmapped while it hides; on Wayland she is gpui's anchored popup, an xdg_popup on the pane's top edge, which the compositor keeps there; gpui moves a popup's anchor never, so a new width that moves her place opens her again. She opens with the sessions' mood, and her face follows the most pressing of their marks: a program that waits surprises her, with a mark and two hops; an error worries her, with a shake; a finish cheers her for 4 s, with a hop; work keeps her eyes on the terminal; calm, she blinks and glances, and dozes after 5 minutes; a press winks at you. Each face is an SVG drawn once, at three times her size so Retina draws her sharp, and she draws again only when her face changes. Settings › Terminal › Ely turns her off; she is on by default.