Files
ELYTerminal/AGENTS.md
T
2026-10-16 21:21:51 -04:00

44 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. 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 and waits on herdr's pipe.
  • 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 (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);
    • input method calls: mark:é composes, commit:é types;
    • 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/sessions.rs: a session and its row. 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/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 7 and the mouse modes before the parser; herdr follows herdr's focused pane through herdr's socket while herdr's client runs in the session: 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 and repeat flag of AppKit's current event, keeps held keys repeating, and hands Command key-ups to the focused view. 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.
  • 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), and shifted punctuation reports the character it types as its key, since gpui clears Shift for it.
  • 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. A full reset, ESC c, empties alacritty's title and sends only CursorBlinkingChange, in the parser's order, synchronized updates too; on that event the view replays the title through set_options, which sends it.
  • 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, and copy on select, 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.