T46: Ely peeks over the main window's top edge from behind it, as the owner asked after the 0.1.3 film: her head and fingers from the icon's paths in a transparent window of her own, kept on the edge as each platform allows, macOS's child window, Windows' owned popup, X11's popup moved and stacked above the frame, Wayland's anchored popup; ten faces follow the most pressing of the sessions' marks, with hops, a shake and a rise; a press winks; Settings, Terminal, Ely turns her off
CI / platforms (windows-latest) (push) Failing after 10s
CI / platforms (ubuntu-24.04) (push) Successful in 1m52s
CI / macos (push) Successful in 12m52s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-18 05:06:59 -04:00
1 parent b35eb52d9a
commit 9aeb3f3552
22 files changed
+1090 -45

No files matched your search

+5 -3
View File
@@ -9,13 +9,13 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- `alacritty_terminal` 0.26: grid, parser, pseudo-terminal, event loop. - `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. - `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 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, and reads the keyboard for win32-input-mode. - 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. - `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 ## Commands
- `cargo run` opens the app. `RUST_LOG=elyterminal=debug cargo run` logs more; `trace` logs each paint. - `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: - `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; - 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; - input method calls: `mark:é` composes, `commit:é` types;
- `wait` pauses a second, so a program can start before the keys after it; - `wait` pauses a second, so a program can start before the keys after it;
@@ -33,6 +33,7 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- `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. - `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/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/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/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. - `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.
@@ -79,7 +80,7 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- 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: 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: 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: 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: 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-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 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: 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.
@@ -106,3 +107,4 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- 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-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: 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: 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, so that the main window seems to hide the rest of her. 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, and is moved as the main window moves; 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. Her face follows the most pressing of the sessions' 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.
+2 -1
View File
@@ -59,8 +59,9 @@ foreign-types = "0.5"
image = { version = "0.25", default-features = false, features = ["png"] } image = { version = "0.25", default-features = false, features = ["png"] }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
raw-window-handle = "0.6"
sysinfo = { version = "0.39", default-features = false, features = ["system"] } sysinfo = { version = "0.39", default-features = false, features = ["system"] }
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_Diagnostics_ToolHelp", "Win32_System_Pipes", "Win32_UI_Input_KeyboardAndMouse"] } windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_Diagnostics_ToolHelp", "Win32_System_Pipes", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_WindowsAndMessaging"] }
[target.'cfg(windows)'.dev-dependencies] [target.'cfg(windows)'.dev-dependencies]
windows-sys = { version = "0.61", features = ["Win32_Security", "Win32_System_IO"] } windows-sys = { version = "0.61", features = ["Win32_Security", "Win32_System_IO"] }
+2
View File
@@ -70,6 +70,7 @@ Ghostty is the bar for what the terminal does. ELYTerminal owns its engine on `a
- The window opens at the size it was left at, on macOS, Windows and Linux: the size is kept in settings once it settles; a maximized or full-screen window keeps the size from before; a kept size past the screen shrinks to it; the window is no smaller than 100 by 100, the least the settings keep. - The window opens at the size it was left at, on macOS, Windows and Linux: the size is kept in settings once it settles; a maximized or full-screen window keeps the size from before; a kept size past the screen shrinks to it; the window is no smaller than 100 by 100, the least the settings keep.
- The title shows the active session's title, and no other text. - The title shows the active session's title, and no other text.
- The pointer over the terminal is the arrow. - The pointer over the terminal is the arrow.
- Ely, the icon's girl, peeks over the window's top edge from behind it, on macOS, Windows and Linux: her face shows how the sessions are doing, waiting, an error, work, a finish or calm, and a press pets her. A switch in Settings, Terminal, turns her off.
- The title bar is transparent, as Ghostty's: the window's background, opacity and blur run under it, so it wears the terminal's color. - The title bar is transparent, as Ghostty's: the window's background, opacity and blur run under it, so it wears the terminal's color.
- Every control in the title bar takes its own press on every platform, the window buttons Ely draws on Windows and Linux too. - Every control in the title bar takes its own press on every platform, the window buttons Ely draws on Windows and Linux too.
@@ -116,6 +117,7 @@ Cmd-, opens it; on Windows and Linux, which have no menu bar, so does a gear in
- Scrollback lines. - Scrollback lines.
- Option as Alt: off, left, right, both (macOS only). - Option as Alt: off, left, right, both (macOS only).
- Copy on select. - Copy on select.
- Ely: a switch.
- Shortcuts: every shortcut, as this platform's keys, with a search. - Shortcuts: every shortcut, as this platform's keys, with a search.
## 11. Background blur ## 11. Background blur
+1
View File
@@ -43,6 +43,7 @@ The app writes its log to stderr. `RUST_LOG=elyterminal=debug` writes more; `tra
- The terminal runs on `alacritty_terminal`: 256 colors and truecolor, bold, dim, italic, five underline styles, wide characters and combining marks. Box drawing, block elements and powerline shapes are drawn as geometry, so lines join across cells. Keys follow xterm and the kitty keyboard protocol, and on Windows win32-input-mode, as Windows Terminal sends them, so herdr's shortcuts work; input methods compose at the cursor; programs can track the mouse; pastes are bracketed when a program asks; OSC 8 and plain links open with the link press below; inside herdr, herdr opens them, web links only, whole where its pane wraps them, and with Shift the terminal opens what it sees; find searches the scrollback. - The terminal runs on `alacritty_terminal`: 256 colors and truecolor, bold, dim, italic, five underline styles, wide characters and combining marks. Box drawing, block elements and powerline shapes are drawn as geometry, so lines join across cells. Keys follow xterm and the kitty keyboard protocol, and on Windows win32-input-mode, as Windows Terminal sends them, so herdr's shortcuts work; input methods compose at the cursor; programs can track the mouse; pastes are bracketed when a program asks; OSC 8 and plain links open with the link press below; inside herdr, herdr opens them, web links only, whole where its pane wraps them, and with Shift the terminal opens what it sees; find searches the scrollback.
- Shell integration for zsh, bash, fish and PowerShell: each prompt reports its folder and title, and OSC 133 marks each prompt, each command and its exit status. - Shell integration for zsh, bash, fish and PowerShell: each prompt reports its folder and title, and OSC 133 marks each prompt, each command and its exit status.
- Ely peeks over the window's top edge. Her face follows your sessions: she startles when a program waits on you, frets at an error, cheers a finish, watches the terminal while a program works, and dozes when all is calm for long. Press her and she winks. Settings › Terminal › Ely turns her off.
- Sessions: new, close and switch. A bell marks a hidden session's row and makes no sound. A row's mark shows what its programs report through OSC 7501, OSC 9;4 or herdr: waiting on you, an error, work with its progress, done or idle, else a failed last command. The mark is 3 by 3 dots: work ripples and waiting dips while they last. Hover over the mark lists each report. - Sessions: new, close and switch. A bell marks a hidden session's row and makes no sound. A row's mark shows what its programs report through OSC 7501, OSC 9;4 or herdr: waiting on you, an error, work with its progress, done or idle, else a failed last command. The mark is 3 by 3 dots: work ripples and waiting dips while they last. Hover over the mark lists each report.
- Notifications: while the window is in the background or the session hidden, a session that starts waiting, finishes or fails, and a program's OSC 9, 777 or 99, post a system notification, one a session every 3 seconds at most; a press shows the session. On macOS only the app bundle posts them, not `cargo run`. - Notifications: while the window is in the background or the session hidden, a session that starts waiting, finishes or fails, and a program's OSC 9, 777 or 99, post a system notification, one a session every 3 seconds at most; a press shows the session. On macOS only the app bundle posts them, not `cargo run`.
- Cmd-Up and Cmd-Down, Ctrl-Shift-Up and Ctrl-Shift-Down off macOS, jump to the prompt before or after the view's top. - Cmd-Up and Cmd-Down, Ctrl-Shift-Up and Ctrl-Shift-Down off macOS, jump to the prompt before or after the view's top.
+1 -1
View File
@@ -115,7 +115,7 @@ Progress truth. Top to bottom, one at a time. A task is ticked only after its co
- [x] T44 herdr's shortcuts work, which a user found dead: `cmd+shift+ctrl+[` and `]` on macOS, and the same keys as herdr reads them on Linux and Windows: under kitty's flags a key Shift makes a symbol of goes as its own key with Shift, the symbol its alternate, on macOS and Windows; on Windows keys go as win32-input-mode records, which ConPTY asks for and herdr reads; on Linux gpui keeps no key code, which the docs say. - [x] T44 herdr's shortcuts work, which a user found dead: `cmd+shift+ctrl+[` and `]` on macOS, and the same keys as herdr reads them on Linux and Windows: under kitty's flags a key Shift makes a symbol of goes as its own key with Shift, the symbol its alternate, on macOS and Windows; on Windows keys go as win32-input-mode records, which ConPTY asks for and herdr reads; on Linux gpui keeps no key code, which the docs say.
- [x] T45 Cmd-press on macOS, Ctrl-press on Windows and Linux opens the link under the pointer, which the owner asked for, wherever the terminal shows one: inside herdr, which wraps long URLs at its pane's edge and passes no OSC 8 target on, while herdr takes the mouse the press goes to herdr, which opens its pane's link whole, web links only, as herdr does, Cmd heard as Ctrl on macOS. - [x] T45 Cmd-press on macOS, Ctrl-press on Windows and Linux opens the link under the pointer, which the owner asked for, wherever the terminal shows one: inside herdr, which wraps long URLs at its pane's edge and passes no OSC 8 target on, while herdr takes the mouse the press goes to herdr, which opens its pane's link whole, web links only, as herdr does, Cmd heard as Ctrl on macOS.
- [ ] T46 Ely peeks over the main window's top edge from behind it, which the owner asked for after the 0.1.3 film, with several faces and moves; a switch in Settings, Terminal, turns her off. - [ ] T46 Ely peeks over the main window's top edge from behind it, which the owner asked for after the 0.1.3 film, with several faces and moves; a switch in Settings, Terminal, turns her off: a window of her own kept on the edge, macOS's child window, Windows' owned popup, X11's popup moved and stacked, Wayland's anchored popup; ten faces from the icon's paths, following the sessions' marks.
## 25. Release ## 25. Release
+34
View File
@@ -21,6 +21,40 @@ pub fn number(window: &gpui::Window) -> anyhow::Result<u32> {
u32::try_from(number).context("window number out of range") u32::try_from(number).context("window number out of range")
} }
/// The window and each of its child windows: number and frame, in AppKit's screen points.
pub fn frames(number: u32) -> anyhow::Result<Vec<(u32, [f64; 4])>> {
use core_graphics::geometry::CGRect;
unsafe {
let app: *mut Object = msg_send![class!(NSApplication), sharedApplication];
let window: *mut Object = msg_send![app, windowWithWindowNumber: i64::from(number)];
anyhow::ensure!(!window.is_null(), "no window {number}");
let children: *mut Object = msg_send![window, childWindows];
let count: u64 = if children.is_null() {
0
} else {
msg_send![children, count]
};
let children = (0..count).map(|ix| msg_send![children, objectAtIndex: ix]);
std::iter::once(window)
.chain(children)
.map(|window: *mut Object| {
let number: i64 = msg_send![window, windowNumber];
let frame: CGRect = msg_send![window, frame];
let corners = [
frame.origin.x,
frame.origin.y,
frame.size.width,
frame.size.height,
];
Ok((
u32::try_from(number).context("window number out of range")?,
corners,
))
})
.collect()
}
}
/// Shows the window as key, the app left inactive. /// Shows the window as key, the app left inactive.
pub fn front(number: u32) -> anyhow::Result<()> { pub fn front(number: u32) -> anyhow::Result<()> {
unsafe { unsafe {
+8
View File
@@ -68,6 +68,14 @@ fn main() {
let title = aim.update(cx, |_, window, _| window.window_title()); let title = aim.update(cx, |_, window, _| window.window_title());
let title = title.expect("the window lives"); let title = title.expect("the window lives");
capture::snapshot(number(aim, cx), std::path::Path::new(&out)).expect("a picture"); capture::snapshot(number(aim, cx), std::path::Path::new(&out)).expect("a picture");
let frames = capture::frames(number(aim, cx)).expect("the window's frames");
log::info!("shot: the window at {:?}", frames[0].1);
for (index, (child, at)) in frames.into_iter().skip(1).enumerate() {
let path = out.replace(".png", &format!(".child{index}.png"));
capture::snapshot(child, std::path::Path::new(&path))
.expect("a child's picture");
log::info!("shot: child window {child} at {at:?}, wrote {path}");
}
let views = capture::views(number(aim, cx)).expect("the window's views"); let views = capture::views(number(aim, cx)).expect("the window's views");
log::info!("shot: wrote {out}, titled {title:?}, over {views:?}"); log::info!("shot: wrote {out}, titled {title:?}, over {views:?}");
for line in capture::menus() { for line in capture::menus() {
+1
View File
@@ -136,6 +136,7 @@ pub fn open(launch: Launch, cx: &mut App) -> anyhow::Result<WindowHandle<Workspa
Workspace::new(pty, events, launch, window, cx) Workspace::new(pty, events, launch, window, cx)
}) })
})?; })?;
crate::pet::follow(window, cx);
log::info!( log::info!(
"app: main window open at {} by {}, blur: {background:?} on {}", "app: main window open at {} by {}, blur: {background:?} on {}",
f32::from(kept.width), f32::from(kept.width),
+1 -36
View File
@@ -1,8 +1,3 @@
use std::{
io::{BufRead, BufReader},
process::{Child, Command, Stdio},
};
use x11rb::{ use x11rb::{
COPY_DEPTH_FROM_PARENT, COPY_DEPTH_FROM_PARENT,
connection::Connection as _, connection::Connection as _,
@@ -10,37 +5,7 @@ use x11rb::{
}; };
use super::*; use super::*;
use crate::testx11::Xvfb;
/// An X server of the test's own, gone with it.
struct Xvfb {
child: Child,
display: String,
}
impl Xvfb {
fn start() -> Self {
let mut child = Command::new("Xvfb")
.args(["-displayfd", "1", "-nolisten", "tcp"])
.stdout(Stdio::piped())
.spawn()
.expect("Xvfb starts: the X11 tests need it");
let mut number = String::new();
let stdout = child.stdout.take().expect("Xvfb's stdout");
BufReader::new(stdout)
.read_line(&mut number)
.expect("Xvfb names its display");
let display = format!(":{}", number.trim());
Self { child, display }
}
}
impl Drop for Xvfb {
fn drop(&mut self) {
if let Err(error) = self.child.kill().and_then(|()| self.child.wait().map(drop)) {
eprintln!("Xvfb {} did not stop: {error}", self.display);
}
}
}
#[test] #[test]
fn the_window_names_its_pane_while_blurred() { fn the_window_names_its_pane_while_blurred() {
+3
View File
@@ -3,10 +3,13 @@ pub mod blur;
pub mod files; pub mod files;
pub mod git; pub mod git;
pub mod menus; pub mod menus;
pub mod pet;
pub mod sessions; pub mod sessions;
pub mod settings; pub mod settings;
pub mod term; pub mod term;
#[cfg(all(test, unix))] #[cfg(all(test, unix))]
mod testlog; mod testlog;
#[cfg(all(test, target_os = "linux"))]
mod testx11;
pub mod themes; pub mod themes;
pub mod workspace; pub mod workspace;
+301
View File
@@ -0,0 +1,301 @@
//! Ely, the icon's girl, peeking over the main window's top edge from a window of her own.
mod faces;
mod look;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(windows)]
mod windows;
#[cfg(target_os = "linux")]
mod x11;
use std::{
cell::RefCell,
f32::consts::PI,
rc::Rc,
sync::Arc,
time::{Duration, Instant},
};
use gpui::{
Animation, AnimationExt as _, App, AppContext as _, Bounds, Context, Decorations, Image,
InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, Render, Styled,
Subscription, Task, Window, WindowBackgroundAppearance, WindowBounds, WindowHandle, WindowKind,
WindowOptions, div, img, point, popup, px, size,
};
#[cfg(target_os = "macos")]
use self::macos as platform;
#[cfg(windows)]
use self::windows as platform;
#[cfg(target_os = "linux")]
use self::x11 as platform;
use self::{
faces::Face,
look::{Mood, PETTED, face, mood},
};
use crate::{settings::SettingsStore, workspace::Workspace};
/// Her head's box, in points, and the room above it her hops take.
const WIDTH: f32 = 112.0;
const HEAD: f32 = 107.4;
const ROOM: f32 = 16.0;
/// How far in from the pane's left edge she holds its top edge.
const LEFT: f32 = 140.0;
/// Ely in her window: her face follows the sessions' marks, and a press pets her.
pub struct Pet {
faces: Vec<(Face, Arc<Image>)>,
mood: Mood,
/// When the mood began.
since: Instant,
petted: Option<Instant>,
/// Counts her moves, so each plays from its start.
moves: usize,
/// What keeps her window on the main window's edge, where the platform does not.
glue: Option<platform::Glue>,
_wake: Option<Task<()>>,
_watch: Vec<Subscription>,
}
/// Shows Ely over `main` while the settings want her.
pub fn follow(main: WindowHandle<Workspace>, cx: &mut App) {
let shown: Rc<RefCell<Option<WindowHandle<Pet>>>> = Rc::default();
let sync = move |cx: &mut App| {
let open = *shown.borrow();
match (SettingsStore::get(cx).ely, open) {
(true, None) => match show(main, cx) {
Ok(pet) => *shown.borrow_mut() = Some(pet),
Err(error) => log::error!("pet: Ely does not show: {error:#}"),
},
(false, Some(pet)) => {
*shown.borrow_mut() = None;
log::info!("pet: Ely goes, as the settings say");
if pet
.update(cx, |_, window, _| window.remove_window())
.is_err()
{
log::debug!("pet: her window had gone");
}
}
_ => {}
}
};
sync(cx);
cx.observe_global::<SettingsStore>(sync).detach();
}
/// Opens her window on `main`'s top edge: a popup Wayland's compositor places, else a window kept there.
fn show(main: WindowHandle<Workspace>, cx: &mut App) -> anyhow::Result<WindowHandle<Pet>> {
let (bounds, inset, wayland) = main.update(cx, |_, window, _| {
(window.bounds(), top_inset(window), wayland(window))
})?;
let room = size(px(WIDTH), px(HEAD + ROOM));
let kind = if wayland {
WindowKind::AnchoredPopup(popup::PopupOptions {
parent: main.into(),
anchor_rect: Bounds::new(point(inset + px(LEFT), inset), size(px(1.), px(1.))),
anchor: popup::PopupAnchor::TopLeft,
gravity: popup::PopupGravity::TopRight,
constraint_adjustment: popup::PopupConstraintAdjustment::empty(),
offset: point(px(0.), px(0.)),
grab: false,
})
} else {
WindowKind::PopUp
};
let origin = point(
bounds.origin.x + inset + px(LEFT),
bounds.origin.y + inset - room.height,
);
let options = WindowOptions {
window_bounds: Some(WindowBounds::Windowed(Bounds::new(origin, room))),
titlebar: None,
focus: false,
show: true,
kind,
is_movable: false,
is_resizable: false,
is_minimizable: false,
window_background: WindowBackgroundAppearance::Transparent,
..Default::default()
};
let workspace = main.entity(cx)?;
let pet = cx.open_window(options, |_, cx| {
cx.new(|cx| {
let feel = cx.observe(&workspace, |pet: &mut Pet, workspace, cx| {
let sessions = workspace.read(cx).sessions();
let now = mood(sessions.iter().filter_map(|session| session.looks(cx)));
if now != pet.mood {
pet.feel(now, cx);
}
});
let mut pet = Pet {
faces: faces::images(),
mood: Mood::Calm,
since: Instant::now(),
petted: None,
moves: 0,
glue: None,
_wake: None,
_watch: vec![feel],
};
pet.wake(cx);
pet
})
})?;
if !wayland {
glue(main, pet, cx)?;
}
let way = if wayland {
"a Wayland popup"
} else {
"a window kept there"
};
log::info!("pet: Ely peeks over the main window, {way}");
Ok(pet)
}
/// Keeps her window on `main`'s top edge as it moves, comes forward and hides.
fn glue(main: WindowHandle<Workspace>, pet: WindowHandle<Pet>, cx: &mut App) -> anyhow::Result<()> {
main.update(cx, |_, main, cx| {
pet.update(cx, |pet, window, cx| {
let glue = platform::Glue::new(window, main)?;
glue.place(main)?;
pet.glue = Some(glue);
pet._watch
.push(cx.observe_window_bounds(main, |pet, main, _| pet.place(main)));
pet._watch
.push(cx.observe_window_activation(main, |pet, main, _| pet.place(main)));
#[cfg(target_os = "linux")]
pet._watch.push(
cx.observe_window_visibility(main, |pet, visibility, main, _| {
pet.shown(visibility.is_visible(), main)
}),
);
anyhow::Ok(())
})?
})?
}
/// The pane's distance from its window's top: the resize margin of a window that draws its own frame and is free at the top.
fn top_inset(window: &Window) -> Pixels {
match (window.window_decorations(), window.client_inset()) {
(Decorations::Client { tiling }, Some(inset)) if !tiling.top => inset,
_ => px(0.),
}
}
/// Whether `window` is a Wayland surface, where the compositor places windows.
#[cfg(target_os = "linux")]
fn wayland(window: &Window) -> bool {
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
HasWindowHandle::window_handle(window)
.is_ok_and(|handle| matches!(handle.as_raw(), RawWindowHandle::Wayland(_)))
}
#[cfg(not(target_os = "linux"))]
fn wayland(_: &Window) -> bool {
false
}
impl Pet {
/// Takes up `mood`: a new face, and the move it starts with.
fn feel(&mut self, mood: Mood, cx: &mut Context<Self>) {
log::debug!("pet: Ely feels {mood:?}, was {:?}", self.mood);
(self.mood, self.since) = (mood, Instant::now());
self.moves += 1;
self.wake(cx);
cx.notify();
}
/// Her face now, a press's wink first.
fn face(&self, now: Instant) -> (Face, Option<Duration>) {
match self.petted.map(|at| now.saturating_duration_since(at)) {
Some(held) if held < PETTED => (Face::Petted, Some(PETTED - held)),
_ => face(self.mood, now.saturating_duration_since(self.since)),
}
}
/// Draws her again when her face changes next.
fn wake(&mut self, cx: &mut Context<Self>) {
let (_, next) = self.face(Instant::now());
self._wake = next.map(|next| {
cx.spawn(async move |pet, cx| {
cx.background_executor().timer(next).await;
if pet
.update(cx, |pet, cx| {
pet.wake(cx);
cx.notify();
})
.is_err()
{
log::debug!("pet: her window went before her next face");
}
})
});
}
fn place(&self, main: &Window) {
if let Some(glue) = &self.glue
&& let Err(error) = glue.place(main)
{
log::error!("{error:#}");
}
}
#[cfg(target_os = "linux")]
fn shown(&self, visible: bool, main: &Window) {
if let Some(glue) = &self.glue {
match glue.shown(visible) {
Ok(()) => log::debug!("pet: Ely shows {visible} with the main window"),
Err(error) => log::error!("{error:#}"),
}
}
if visible {
self.place(main);
}
}
}
impl Render for Pet {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let (face, _) = self.face(Instant::now());
let image = (self.faces.iter())
.find(|(drawn, _)| *drawn == face)
.map(|(_, image)| image.clone())
.expect("every face is drawn");
let (mood, first) = (self.mood, self.moves == 0);
let head = img(image)
.absolute()
.left_0()
.bottom_0()
.w(px(WIDTH))
.h(px(HEAD))
.with_animation(
("ely", self.moves),
Animation::new(Duration::from_millis(600)),
move |head, delta| match mood {
// She rises from behind the edge when she comes.
_ if first => head.bottom(px(-HEAD * (1.0 - delta).powi(3))),
// A hop: two for a program that waits, one for one that finished.
Mood::Waiting | Mood::Done => {
let hops = if mood == Mood::Waiting { 2.0 } else { 1.0 };
head.bottom(px((delta * hops * PI).sin().abs() * (ROOM - 4.0)))
}
// A shake that dies away.
Mood::Error => head.left(px((delta * 6.0 * PI).sin() * 4.0 * (1.0 - delta))),
Mood::Calm | Mood::Working => head,
},
);
div().size_full().relative().child(head).on_mouse_down(
MouseButton::Left,
cx.listener(|pet, _, _, cx| {
log::debug!("pet: Ely is pressed");
pet.petted = Some(Instant::now());
pet.wake(cx);
cx.notify();
}),
)
}
}
+182
View File
@@ -0,0 +1,182 @@
//! Ely's faces: the icon's head, cut at the edge her fingers hold, one SVG a face.
use std::sync::Arc;
use gpui::{Image, ImageFormat};
/// What Ely's face shows.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Face {
Calm,
Blink,
/// Eyes on the terminal below her, as while you type or a program works.
Down,
Left,
Right,
/// A program finished: eyes shut in a smile, and sparkles.
Happy,
/// A program waits on you: wide eyes, an open mouth, and a mark.
Surprised,
/// A program failed: raised brows and a drop.
Worried,
Sleepy,
/// Pressed: a wink and a heart.
Petted,
}
impl Face {
pub const ALL: [Face; 10] = [
Face::Calm,
Face::Blink,
Face::Down,
Face::Left,
Face::Right,
Face::Happy,
Face::Surprised,
Face::Worried,
Face::Sleepy,
Face::Petted,
];
}
/// The head's box in the icon's units; the edge she holds runs along its bottom.
const VIEW: [u32; 4] = [110, 6, 292, 280];
/// Pixels a unit draws at: three, so a Retina screen draws her sharp.
const SCALE: u32 = 3;
/// Every face as an image gpui draws once and keeps.
pub fn images() -> Vec<(Face, Arc<Image>)> {
Face::ALL
.into_iter()
.map(|face| {
let svg = svg(face).into_bytes();
(face, Arc::new(Image::from_bytes(ImageFormat::Svg, svg)))
})
.collect()
}
/// The SVG of `face`.
pub fn svg(face: Face) -> String {
let (eyes, mouth, mark) = match face {
Face::Calm => (open((0, 0), 1.0), SMILE, ""),
Face::Blink => (SHUT.into(), SMILE, ""),
Face::Down => (open((0, 5), 1.0), FLAT, ""),
Face::Left => (open((-5, 0), 1.0), SMILE, ""),
Face::Right => (open((5, 0), 1.0), SMILE, ""),
Face::Happy => (ARCS.into(), GRIN, SPARKLES),
Face::Surprised => (open((0, 0), 1.12), GASP, BANG),
Face::Worried => (open((0, 3), 1.0), FROWN, DROP),
Face::Sleepy => (LINES.into(), FLAT, SNORE),
Face::Petted => (wink(), GRIN, HEART),
};
let blush = if face == Face::Petted { FLUSH } else { BLUSH };
let brows = if face == Face::Worried { BROWS } else { "" };
let [x, y, w, h] = VIEW;
let (width, height) = (w * SCALE, h * SCALE);
format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="{x} {y} {w} {h}">{HEAD}{eyes}{blush}{mouth}{HAIR}{brows}{HANDS}{mark}</svg>"#
)
}
/// An eye at `cx`, its iris moved by `look`, the whole eye grown by `wide`.
fn eye(cx: i32, (dx, dy): (i32, i32), wide: f32) -> String {
format!(
r##"<g transform="translate({cx} 188) scale({wide}) translate({} -188)"><ellipse cx="{cx}" cy="188" rx="18" ry="23" fill="#A24FB9"/><g transform="translate({dx} {dy})"><ellipse cx="{cx}" cy="198" rx="13.5" ry="11.5" fill="#EE84C4"/><ellipse cx="{cx}" cy="190" rx="7.5" ry="10.5" fill="#5A2368"/><circle cx="{}" cy="178" r="7" fill="#FFFFFF"/><circle cx="{}" cy="199" r="3.2" fill="#FFFFFF"/></g></g>"##,
-cx,
cx - 7,
cx + 7
)
}
/// Both eyes open, with their lids and lashes.
fn open(look: (i32, i32), wide: f32) -> String {
format!(
"{}{}{LEFT_LID}{RIGHT_LID}{LASHES}",
eye(222, look, wide),
eye(290, look, wide)
)
}
/// The left eye shut in a smile, the right one open.
fn wink() -> String {
format!("{}{RIGHT_LID}{LEFT_ARC}", eye(290, (0, 0), 1.0))
}
const LEFT_LID: &str = r##"<path fill="#45233F" d="M194 182C195 168 208 159 224 159C234 159 241 164 244 172L240 173C236 168 231 165 224 165C214 165 207 172 204 183Z"/>"##;
const RIGHT_LID: &str = r##"<path fill="#45233F" d="M318 182C317 168 304 159 288 159C278 159 271 164 268 172L272 173C276 168 281 165 288 165C298 165 305 172 308 183Z"/>"##;
const LASHES: &str = r##"<path fill="none" stroke="#C2506F" stroke-width="2" stroke-linecap="round" opacity=".35" d="M215 212Q222 214 229 212M283 212Q290 214 297 212"/>"##;
const LEFT_ARC: &str = r##"<path fill="none" stroke="#45233F" stroke-width="7" stroke-linecap="round" d="M205 197Q222 176 239 197"/>"##;
const SHUT: &str = r##"<g fill="none" stroke="#45233F" stroke-width="6" stroke-linecap="round"><path d="M205 192Q222 203 239 192"/><path d="M273 192Q290 203 307 192"/></g>"##;
const ARCS: &str = r##"<g fill="none" stroke="#45233F" stroke-width="7" stroke-linecap="round"><path d="M205 197Q222 176 239 197"/><path d="M273 197Q290 176 307 197"/></g>"##;
const LINES: &str = r##"<g fill="none" stroke="#45233F" stroke-width="6" stroke-linecap="round"><path d="M205 194H239"/><path d="M273 194H307"/></g>"##;
const BROWS: &str = r##"<g fill="none" stroke="#45233F" stroke-width="5" stroke-linecap="round"><path d="M206 160L234 152"/><path d="M306 160L278 152"/></g>"##;
const SMILE: &str = r##"<path fill="none" stroke="#C2506F" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" d="M244 222Q250 229 256 223Q262 229 268 222"/>"##;
const GRIN: &str = r##"<path fill="#C2506F" d="M243 219Q256 241 269 219Z"/><path fill="#FF8DAA" d="M249 229Q256 236 263 229Q256 225 249 229Z"/>"##;
const GASP: &str = r##"<ellipse cx="256" cy="226" rx="5.5" ry="7" fill="#C2506F"/>"##;
const FLAT: &str = r##"<path fill="none" stroke="#C2506F" stroke-width="3.5" stroke-linecap="round" d="M249 225H263"/>"##;
const FROWN: &str = r##"<path fill="none" stroke="#C2506F" stroke-width="3.5" stroke-linecap="round" d="M246 228Q256 220 266 228"/>"##;
const BLUSH: &str = r##"<g opacity=".55" fill="#FF8DAA"><ellipse cx="198" cy="216" rx="15" ry="8"/><ellipse cx="314" cy="216" rx="15" ry="8"/></g>"##;
const FLUSH: &str = r##"<g opacity=".85" fill="#FF7A9C"><ellipse cx="198" cy="216" rx="17" ry="9"/><ellipse cx="314" cy="216" rx="17" ry="9"/></g>"##;
const BANG: &str = r##"<g fill="#FFB020" stroke="#45233F" stroke-width="3"><path d="M371 38L385 38L380 86L376 86Z"/><circle cx="378" cy="100" r="7"/></g>"##;
const SNORE: &str = r##"<g fill="none" stroke="#9FA3D9" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"><path d="M352 70H372L352 92H372"/><path d="M380 38H394L380 54H394"/></g>"##;
const HEART: &str = r##"<path fill="#FF5C93" d="M378 70C366 56 350 66 356 80C360 90 378 100 378 100C378 100 396 90 400 80C406 66 390 56 378 70Z"/>"##;
const SPARKLES: &str = r##"<g fill="#FFE48A"><path d="M140 70Q143 84 156 86Q143 88 140 102Q137 88 124 86Q137 84 140 70Z"/><path d="M380 52Q382 62 392 64Q382 66 380 76Q378 66 368 64Q378 62 380 52Z"/></g>"##;
const DROP: &str = r##"<path fill="#9CD5FF" stroke="#5AA0D8" stroke-width="2" d="M346 128Q356 144 350 152Q340 156 338 146Q338 138 346 128Z"/>"##;
/// Her hair behind, her collar, and her face: the icon's paths.
const HEAD: &str = concat!(
r##"<defs><clipPath id="face"><path d="M178 164C178 116 212 94 256 94C300 94 334 116 334 164C334 202 312 228 284 238C274 242 264 244 256 244C248 244 238 242 228 238C200 228 178 202 178 164Z"/></clipPath></defs>"##,
r##"<path fill="#EC86B5" d="M256 46C192 46 142 90 136 158C131 214 126 262 118 306L394 306C386 262 381 214 376 158C370 90 320 46 256 46Z"/>"##,
r##"<path fill="#D9659A" d="M194 200C198 238 214 262 230 276L282 276C298 262 314 238 318 200Z"/>"##,
r##"<path fill="#F7CDBE" d="M236 222L276 222L276 266C268 270 244 270 236 266Z"/>"##,
r##"<path fill="#FFFFFF" d="M198 284C204 268 222 258 238 256L256 272L274 256C290 258 308 268 314 284Z"/>"##,
r##"<path fill="#F05A97" d="M256 264C244 250 226 248 224 258C222 270 236 278 256 264ZM256 264C268 250 286 248 288 258C290 270 276 278 256 264Z"/>"##,
r##"<circle cx="256" cy="264" r="8" fill="#D94683"/>"##,
r##"<path fill="#FFE7DC" d="M178 164C178 116 212 94 256 94C300 94 334 116 334 164C334 202 312 228 284 238C274 242 264 244 256 244C248 244 238 242 228 238C200 228 178 202 178 164Z"/>"##,
r##"<g clip-path="url(#face)"><path fill="#F9D8CB" transform="translate(0 12)" d="M170 176C162 110 204 62 256 62C308 62 350 110 342 176Q328 152 318 134Q316 156 304 172Q298 150 286 130Q286 156 274 178Q268 150 256 128Q244 150 238 178Q226 156 226 130Q214 150 208 172Q196 156 194 134Q184 152 170 176Z"/></g>"##,
);
/// Her side locks, bangs, ahoge and the star in her hair: the icon's paths.
const HAIR: &str = concat!(
r##"<path fill="#F7A6C9" d="M184 118C164 150 156 204 160 258C162 282 156 300 148 316C164 312 178 300 186 286C190 256 192 214 198 176C200 152 196 132 184 118ZM328 118C348 150 356 204 352 258C350 282 356 300 364 316C348 312 334 300 326 286C322 256 320 214 314 176C312 152 316 132 328 118Z"/>"##,
r##"<path fill="#F7A6C9" d="M170 176C162 110 204 62 256 62C308 62 350 110 342 176Q328 152 318 134Q316 156 304 172Q298 150 286 130Q286 156 274 178Q268 150 256 128Q244 150 238 178Q226 156 226 130Q214 150 208 172Q196 156 194 134Q184 152 170 176Z"/>"##,
r##"<path fill="#FFD1E4" d="M196 108C220 84 292 84 316 108C292 92 220 92 196 108Z"/>"##,
r##"<path fill="#F7A6C9" d="M250 66C238 40 252 18 280 20C264 28 258 44 262 64Z"/>"##,
r##"<path fill="#FFE48A" d="M318 86Q320 98 332 100Q320 102 318 114Q316 102 304 100Q316 98 318 86Z"/>"##,
);
/// Her fingers over the edge she holds.
const HANDS: &str = concat!(
r##"<path fill="#FFE7DC" d="M150 286L150 272C150 264 161 264 161 272C161 262 172 262 172 271C172 262 183 262 183 272C183 264 194 264 194 273L194 286ZM318 286L318 273C318 264 329 264 329 272C329 262 340 262 340 271C340 262 351 262 351 272C351 264 362 264 362 272L362 286Z"/>"##,
r##"<path fill="none" stroke="#F2BBA8" stroke-width="2" stroke-linecap="round" d="M161 274V286M172 273V286M183 274V286M329 274V286M340 273V286M351 274V286"/>"##,
);
#[cfg(test)]
mod tests {
use resvg::usvg;
use super::*;
#[test]
fn every_face_draws_at_three_times_her_box() {
for face in Face::ALL {
let svg = svg(face);
let tree = usvg::Tree::from_str(&svg, &usvg::Options::default())
.unwrap_or_else(|error| panic!("{face:?}: {error}"));
assert_eq!(tree.size().width(), 292.0 * 3.0, "{face:?}");
assert_eq!(tree.size().height(), 280.0 * 3.0, "{face:?}");
}
assert_eq!(images().len(), Face::ALL.len());
}
#[test]
fn each_face_shows_what_it_says() {
assert!(svg(Face::Surprised).contains(BANG) && svg(Face::Surprised).contains(GASP));
assert!(svg(Face::Worried).contains(BROWS) && svg(Face::Worried).contains(DROP));
assert!(svg(Face::Happy).contains(ARCS) && svg(Face::Happy).contains(SPARKLES));
assert!(svg(Face::Sleepy).contains(LINES) && svg(Face::Sleepy).contains(SNORE));
assert!(svg(Face::Petted).contains(HEART) && svg(Face::Petted).contains(FLUSH));
assert!(svg(Face::Blink).contains(SHUT) && !svg(Face::Blink).contains(LEFT_LID));
assert_ne!(svg(Face::Left), svg(Face::Right));
}
}
+149
View File
@@ -0,0 +1,149 @@
//! What Ely feels from the sessions' marks, and the face she makes at each moment of it.
use std::time::Duration;
use super::faces::Face;
use crate::sessions::orb::Look;
/// What the sessions ask of her, least pressing first.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Mood {
Calm,
Done,
Working,
Error,
Waiting,
}
/// The most pressing of the marks' looks; calm with none.
pub fn mood(looks: impl IntoIterator<Item = Look>) -> Mood {
looks
.into_iter()
.map(|look| match look {
Look::Waiting => Mood::Waiting,
Look::Error => Mood::Error,
Look::Working(_) => Mood::Working,
Look::Done => Mood::Done,
Look::Idle => Mood::Calm,
})
.max()
.unwrap_or(Mood::Calm)
}
/// How long she cheers a program that finished, and how long calm lasts before she dozes.
const CHEER: Duration = Duration::from_secs(4);
const DOZE: Duration = Duration::from_secs(300);
/// How long a press keeps her wink.
pub const PETTED: Duration = Duration::from_millis(1500);
/// A calm round of 20 s: she blinks twice and glances left, then right.
const CALM: (f32, [(f32, Face); 9]) = (
20.0,
[
(0.0, Face::Calm),
(4.0, Face::Blink),
(4.15, Face::Calm),
(9.0, Face::Left),
(10.2, Face::Calm),
(13.0, Face::Blink),
(13.15, Face::Calm),
(16.0, Face::Right),
(17.2, Face::Calm),
],
);
/// A working round of 8 s: her eyes stay on the terminal but for a blink.
const WORKING: (f32, [(f32, Face); 3]) = (
8.0,
[(0.0, Face::Down), (5.0, Face::Blink), (5.15, Face::Down)],
);
/// Her face `held` into `mood`, and how long it holds; none while it holds for good.
pub fn face(mood: Mood, held: Duration) -> (Face, Option<Duration>) {
match mood {
Mood::Waiting => (Face::Surprised, None),
Mood::Error => (Face::Worried, None),
Mood::Done if held < CHEER => (Face::Happy, Some(CHEER - held)),
Mood::Working => round(WORKING.0, &WORKING.1, held),
Mood::Calm | Mood::Done if held >= DOZE => (Face::Sleepy, None),
Mood::Calm | Mood::Done => {
let (face, next) = round(CALM.0, &CALM.1, held);
(face, next.map(|next| next.min(DOZE - held)))
}
}
}
/// The face a round of `period` seconds shows `held` into it, and how long it holds.
fn round(period: f32, steps: &[(f32, Face)], held: Duration) -> (Face, Option<Duration>) {
let at = held.as_secs_f32() % period;
let now = steps
.iter()
.rposition(|&(start, _)| start <= at)
.expect("a round starts at 0");
let end = steps.get(now + 1).map_or(period, |&(start, _)| start);
(steps[now].1, Some(Duration::from_secs_f32(end - at)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_most_pressing_mark_sets_her_mood() {
assert_eq!(mood([]), Mood::Calm);
assert_eq!(mood([Look::Idle, Look::Done]), Mood::Done);
assert_eq!(
mood([Look::Done, Look::Working(Some(40)), Look::Idle]),
Mood::Working
);
assert_eq!(mood([Look::Working(None), Look::Error]), Mood::Error);
assert_eq!(
mood([Look::Error, Look::Waiting, Look::Done]),
Mood::Waiting
);
}
#[test]
fn calm_blinks_and_glances_then_dozes() {
let at = |secs: f32| face(Mood::Calm, Duration::from_secs_f32(secs)).0;
assert_eq!(at(1.0), Face::Calm);
assert_eq!(at(4.1), Face::Blink);
assert_eq!(at(9.5), Face::Left);
assert_eq!(at(16.5), Face::Right);
assert_eq!(at(24.1), Face::Blink, "the round comes again");
assert_eq!(at(300.0), Face::Sleepy);
assert_eq!(
face(Mood::Calm, Duration::from_secs(400)).1,
None,
"she sleeps on"
);
let (_, wait) = face(Mood::Calm, Duration::from_secs_f32(1.0));
assert!((wait.expect("a next face").as_secs_f32() - 3.0).abs() < 1e-3);
}
#[test]
fn a_finished_program_cheers_her_then_she_calms() {
let (face_now, wait) = face(Mood::Done, Duration::from_secs(1));
assert_eq!(face_now, Face::Happy);
assert_eq!(wait, Some(Duration::from_secs(3)));
assert_eq!(face(Mood::Done, Duration::from_secs(5)).0, Face::Calm);
}
#[test]
fn waiting_and_errors_hold_their_faces() {
assert_eq!(
face(Mood::Waiting, Duration::from_secs(999)),
(Face::Surprised, None)
);
assert_eq!(face(Mood::Error, Duration::ZERO), (Face::Worried, None));
assert_eq!(
face(Mood::Working, Duration::from_secs_f32(5.1)).0,
Face::Blink
);
assert_eq!(
face(Mood::Working, Duration::from_secs(1000)).0,
Face::Down,
"work never dozes"
);
}
}
+78
View File
@@ -0,0 +1,78 @@
//! Ely's window on macOS: a child of the main window at its level, so it moves with it and keeps its layer and Space.
use anyhow::bail;
use gpui::Window;
use objc::{
msg_send,
runtime::{NO, Object},
sel, sel_impl,
};
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
use super::LEFT;
/// AppKit's `NSWindowAbove`.
const ABOVE: isize = 1;
#[repr(C)]
#[derive(Clone, Copy)]
struct Point {
x: f64,
y: f64,
}
#[repr(C)]
#[derive(Clone, Copy)]
struct Rect {
origin: Point,
width: f64,
height: f64,
}
/// Her NSWindow and the main one's.
pub struct Glue {
pet: *mut Object,
main: *mut Object,
}
impl Glue {
/// Makes her window the main window's child, at its level and in its Spaces, with no shadow.
pub fn new(pet: &Window, main: &Window) -> anyhow::Result<Self> {
let (pet, main) = (native(pet)?, native(main)?);
unsafe {
let level: isize = msg_send![main, level];
let _: () = msg_send![pet, setLevel: level];
let spaces: usize = msg_send![main, collectionBehavior];
let _: () = msg_send![pet, setCollectionBehavior: spaces];
let _: () = msg_send![pet, setHasShadow: NO];
let _: () = msg_send![main, addChildWindow: pet ordered: ABOVE];
}
Ok(Self { pet, main })
}
/// Stands her on the main window's top edge, `LEFT` points in from its left.
pub fn place(&self, _: &Window) -> anyhow::Result<()> {
unsafe {
let frame: Rect = msg_send![self.main, frame];
let at = Point {
x: frame.origin.x + f64::from(LEFT),
y: frame.origin.y + frame.height,
};
let _: () = msg_send![self.pet, setFrameOrigin: at];
}
Ok(())
}
}
/// The NSWindow gpui made for `window`.
fn native(window: &Window) -> anyhow::Result<*mut Object> {
let handle = HasWindowHandle::window_handle(window)
.map_err(|error| anyhow::anyhow!("pet: no native handle: {error:?}"))?;
let RawWindowHandle::AppKit(appkit) = handle.as_raw() else {
bail!("pet: not an AppKit window");
};
let view = appkit.ns_view.as_ptr() as *mut Object;
let native: *mut Object = unsafe { msg_send![view, window] };
anyhow::ensure!(!native.is_null(), "pet: the view is in no window");
Ok(native)
}
+66
View File
@@ -0,0 +1,66 @@
//! Ely's window on Windows: owned by the main window, so it stays above it and hides with it, and moved as it moves.
use anyhow::bail;
use gpui::Window;
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
use windows_sys::Win32::{
Foundation::{HWND, RECT},
UI::WindowsAndMessaging::{
GWLP_HWNDPARENT, GetWindowRect, HWND_NOTOPMOST, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE,
SWP_NOZORDER, SetWindowLongPtrW, SetWindowPos,
},
};
use super::{HEAD, LEFT, ROOM};
/// Her window and the main one.
pub struct Glue {
pet: HWND,
main: HWND,
}
impl Glue {
/// Gives her window the main window as owner, and takes away the topmost place gpui gives a popup.
pub fn new(pet: &Window, main: &Window) -> anyhow::Result<Self> {
let (pet, main) = (native(pet)?, native(main)?);
unsafe {
SetWindowLongPtrW(pet, GWLP_HWNDPARENT, main as isize);
let flags = SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE;
anyhow::ensure!(
SetWindowPos(pet, HWND_NOTOPMOST, 0, 0, 0, 0, flags) != 0,
"pet: her window keeps the topmost place"
);
}
Ok(Self { pet, main })
}
/// Stands her on the main window's top edge, `LEFT` points in from its left.
pub fn place(&self, main: &Window) -> anyhow::Result<()> {
let scale = main.scale_factor();
let mut frame = RECT::default();
unsafe {
anyhow::ensure!(
GetWindowRect(self.main, &mut frame) != 0,
"pet: the main window's place does not read"
);
let x = frame.left + (LEFT * scale).round() as i32;
let y = frame.top - ((HEAD + ROOM) * scale).round() as i32;
let flags = SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE;
anyhow::ensure!(
SetWindowPos(self.pet, std::ptr::null_mut(), x, y, 0, 0, flags) != 0,
"pet: her window does not move"
);
}
Ok(())
}
}
/// The HWND gpui made for `window`.
fn native(window: &Window) -> anyhow::Result<HWND> {
let handle = HasWindowHandle::window_handle(window)
.map_err(|error| anyhow::anyhow!("pet: no native handle: {error:?}"))?;
let RawWindowHandle::Win32(win32) = handle.as_raw() else {
bail!("pet: not a Win32 window");
};
Ok(win32.hwnd.get() as HWND)
}
+193
View File
@@ -0,0 +1,193 @@
//! Ely's window on X11: gpui's popup, which no window manager places, put on the main window's top edge and just above it.
use anyhow::{Context as _, bail};
use gpui::Window;
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
use x11rb::{
protocol::xproto::{ConfigureWindowAux, ConnectionExt as _, StackMode},
rust_connection::RustConnection,
};
use super::{HEAD, LEFT, ROOM, top_inset};
/// Her X window, the main one, and a connection of her own to their server.
pub struct Glue {
connection: RustConnection,
pet: u32,
main: u32,
}
impl Glue {
pub fn new(pet: &Window, main: &Window) -> anyhow::Result<Self> {
let (pet, main) = (native(pet)?, native(main)?);
// gpui opens $DISPLAY too; a window's place belongs to whoever asks.
let (connection, _) = x11rb::connect(None).context("pet: no X server")?;
Ok(Self {
connection,
pet,
main,
})
}
/// Stands her on the main window's pane, `LEFT` points in, stacked just above the main window's frame.
pub fn place(&self, main: &Window) -> anyhow::Result<()> {
self.put(main.scale_factor(), f32::from(top_inset(main)))
}
/// Places her at `scale` over a pane `inset` points into the main window.
fn put(&self, scale: f32, inset: f32) -> anyhow::Result<()> {
let (root, frame) = self.frame()?;
let at = (self.connection)
.translate_coordinates(self.main, root, 0, 0)?
.reply()
.context("pet: the main window's place")?;
let aux = ConfigureWindowAux::new()
.x(i32::from(at.dst_x) + ((LEFT + inset) * scale).round() as i32)
.y(i32::from(at.dst_y) + ((inset - HEAD - ROOM) * scale).round() as i32)
.sibling(frame)
.stack_mode(StackMode::ABOVE);
(self.connection)
.configure_window(self.pet, &aux)?
.check()
.context("pet: the X server did not move her window")
}
/// Maps her window while the main one shows, and unmaps it while that hides.
pub fn shown(&self, visible: bool) -> anyhow::Result<()> {
let cookie = if visible {
self.connection.map_window(self.pet)?
} else {
self.connection.unmap_window(self.pet)?
};
cookie
.check()
.context("pet: her window did not map or unmap")
}
/// The root, and the main window's top window under it: the frame a window manager put round it, or itself.
fn frame(&self) -> anyhow::Result<(u32, u32)> {
let mut window = self.main;
loop {
let tree = self.connection.query_tree(window)?.reply()?;
if tree.parent == tree.root {
return Ok((tree.root, window));
}
window = tree.parent;
}
}
}
/// The X window gpui made for `window`.
fn native(window: &Window) -> anyhow::Result<u32> {
let handle = HasWindowHandle::window_handle(window)
.map_err(|error| anyhow::anyhow!("pet: no native handle: {error:?}"))?;
let RawWindowHandle::Xcb(xcb) = handle.as_raw() else {
bail!("pet: not an X11 window");
};
Ok(xcb.window.get())
}
#[cfg(test)]
mod tests {
use x11rb::{
COPY_DEPTH_FROM_PARENT,
connection::Connection as _,
protocol::xproto::{CreateWindowAux, MapState, WindowClass},
};
use super::*;
use crate::testx11::Xvfb;
#[test]
fn she_stands_on_the_pane_just_above_the_frame_and_hides_with_it() {
let xvfb = Xvfb::start();
let (connection, screen) = x11rb::connect(Some(&xvfb.display)).expect("the test connects");
let root = connection.setup().roots[screen].root;
let window =
|parent, (x, y, width, height): (i16, i16, u16, u16), aux: &CreateWindowAux| {
let id = connection.generate_id().expect("a window id");
let class = WindowClass::INPUT_OUTPUT;
(connection.create_window(
COPY_DEPTH_FROM_PARENT,
id,
parent,
x,
y,
width,
height,
0,
class,
0,
aux,
))
.expect("the request")
.check()
.expect("a window");
connection
.map_window(id)
.expect("the request")
.check()
.expect("mapped");
id
};
let plain = CreateWindowAux::new();
let pet = window(
root,
(0, 0, 112, 124),
&CreateWindowAux::new().override_redirect(1),
);
// A window manager's frame, its title bar 20 pixels tall, round the main window.
let frame = window(root, (100, 80, 800, 620), &plain);
let main = window(frame, (0, 20, 800, 600), &plain);
window(root, (0, 0, 50, 50), &plain);
let hers = x11rb::connect(Some(&xvfb.display)).expect("hers").0;
let glue = Glue {
connection: hers,
pet,
main,
};
glue.put(2.0, 10.0).expect("she is placed");
let check = x11rb::connect(Some(&xvfb.display))
.expect("the check connects")
.0;
let at = check
.get_geometry(pet)
.expect("the request")
.reply()
.expect("her place");
let left = (
100 + ((LEFT + 10.0) * 2.0).round() as i32,
100 + ((10.0 - HEAD - ROOM) * 2.0).round() as i32,
);
assert_eq!((i32::from(at.x), i32::from(at.y)), left);
let order = check
.query_tree(root)
.expect("the request")
.reply()
.expect("the stack")
.children;
let place = |id| {
order
.iter()
.position(|&child| child == id)
.expect("on the root")
};
assert_eq!(
place(pet),
place(frame) + 1,
"just above the frame: {order:?}"
);
let mapped = |check: &x11rb::rust_connection::RustConnection| {
check
.get_window_attributes(pet)
.expect("the request")
.reply()
.expect("her state")
.map_state
};
glue.shown(false).expect("hidden");
assert_eq!(mapped(&check), MapState::UNMAPPED);
glue.shown(true).expect("shown");
assert_ne!(mapped(&check), MapState::UNMAPPED);
}
}
+8 -3
View File
@@ -39,11 +39,16 @@ pub struct Session {
} }
impl Session { impl Session {
/// Notes the look its mark takes `now`; true while the mark moves. /// The look its mark takes from its terminal now.
pub fn note(&mut self, now: Instant, cx: &App) -> bool { pub fn looks(&self, cx: &App) -> Option<Look> {
let view = self.terminal.read(cx); let view = self.terminal.read(cx);
let failed = view.ended().is_some_and(|code| code != 0); let failed = view.ended().is_some_and(|code| code != 0);
let shows = look(view.records().shown().map(|(_, record)| record), failed); look(view.records().shown().map(|(_, record)| record), failed)
}
/// Notes the look its mark takes `now`; true while the mark moves.
pub fn note(&mut self, now: Instant, cx: &App) -> bool {
let shows = self.looks(cx);
if self.look.map(|(shown, _)| shown) != shows { if self.look.map(|(shown, _)| shown) != shows {
self.look = shows.map(|shows| (shows, now)); self.look = shows.map(|shows| (shows, now));
} }
+3
View File
@@ -72,6 +72,8 @@ pub struct Settings {
pub scrollback: u32, pub scrollback: u32,
pub option_as_alt: OptionAsAlt, pub option_as_alt: OptionAsAlt,
pub copy_on_select: bool, pub copy_on_select: bool,
/// Ely peeks over the main window's top edge.
pub ely: bool,
pub show_sessions: bool, pub show_sessions: bool,
pub show_panel: bool, pub show_panel: bool,
pub panel: PanelTab, pub panel: PanelTab,
@@ -103,6 +105,7 @@ impl Default for Settings {
scrollback: 10_000, scrollback: 10_000,
option_as_alt: OptionAsAlt::Off, option_as_alt: OptionAsAlt::Off,
copy_on_select: false, copy_on_select: false,
ely: true,
show_sessions: true, show_sessions: true,
show_panel: true, show_panel: true,
panel: PanelTab::Git, panel: PanelTab::Git,
+8 -1
View File
@@ -99,7 +99,14 @@ impl SettingsWindow {
settings.copy_on_select, settings.copy_on_select,
|settings, on| settings.copy_on_select = on, |settings, on| settings.copy_on_select = on,
); );
let section = section.row(SettingsRow::new("Copy on select").control(copy)); let ely = switch(&this, "ely", settings.ely, |settings, on| settings.ely = on);
let section = section
.row(SettingsRow::new("Copy on select").control(copy))
.row(
SettingsRow::new("Ely")
.description("Peeks over the window, her face as your sessions are")
.control(ely),
);
let keys = SettingsSection::new("Shortcuts") let keys = SettingsSection::new("Shortcuts")
.row(KeyboardShortcutsList::new(shortcuts(cx), &self.search)); .row(KeyboardShortcutsList::new(shortcuts(cx), &self.search));
div().flex().flex_col().gap_6().child(section).child(keys) div().flex().flex_col().gap_6().child(section).child(keys)
+37
View File
@@ -0,0 +1,37 @@
//! Xvfb for the X11 tests.
use std::{
io::{BufRead, BufReader},
process::{Child, Command, Stdio},
};
/// An X server of the test's own, gone with it.
pub struct Xvfb {
child: Child,
pub display: String,
}
impl Xvfb {
pub fn start() -> Self {
let mut child = Command::new("Xvfb")
.args(["-displayfd", "1", "-nolisten", "tcp"])
.stdout(Stdio::piped())
.spawn()
.expect("Xvfb starts: the X11 tests need it");
let mut number = String::new();
let stdout = child.stdout.take().expect("Xvfb's stdout");
BufReader::new(stdout)
.read_line(&mut number)
.expect("Xvfb names its display");
let display = format!(":{}", number.trim());
Self { child, display }
}
}
impl Drop for Xvfb {
fn drop(&mut self) {
if let Err(error) = self.child.kill().and_then(|()| self.child.wait().map(drop)) {
eprintln!("Xvfb {} did not stop: {error}", self.display);
}
}
}
+5
View File
@@ -263,6 +263,11 @@ impl Workspace {
} }
} }
/// Its sessions, in their rows' order.
pub fn sessions(&self) -> &[Session] {
&self.sessions
}
/// The shown session's title, or the app's name while its program names none. /// The shown session's title, or the app's name while its program names none.
fn title(&self) -> &str { fn title(&self) -> &str {
self.sessions self.sessions
+2
View File
@@ -84,6 +84,7 @@ What no check reached: no real KWin drew the blur; of Wayland desktops, only Hyp
| The pointer over the terminal is the arrow | T26 | the terminal sets no pointer, so gpui's arrow shows; on X11 with the pointer over the grid, gpui asked for no I-beam ("text"), which it had asked for, and failed to load under Xvfb, before | Pass | | The pointer over the terminal is the arrow | T26 | the terminal sets no pointer, so gpui's arrow shows; on X11 with the pointer over the grid, gpui asked for no I-beam ("text"), which it had asked for, and failed to load under Xvfb, before | Pass |
| The title bar is transparent | T11, T19 | T19 review | Pass | | The title bar is transparent | T11, T19 | T19 review | Pass |
| Every title bar control takes its press, Ely's window buttons on Windows and Linux too | T26 | Ely `cdc5ce8`: `window_controls_occlude_what_lies_under` (each style; before, the box under Windows' caption buttons took the press), `a_wobbling_press_on_a_slot_moves_no_window` (before, the window moved), `a_wobbling_press_on_a_column_title_moves_the_window`; on the owner's PC, window messages asked the app for Windows' hit test along the title bar: v0.1.0 answered caption from end to end, T26's build answers client over the toggles and +, caption over the titles, min, max and close over Ely's buttons, and a press on close closed the app; the owner, given v0.1.1's build and these controls to click, reported it works on Windows | Pass | | Every title bar control takes its press, Ely's window buttons on Windows and Linux too | T26 | Ely `cdc5ce8`: `window_controls_occlude_what_lies_under` (each style; before, the box under Windows' caption buttons took the press), `a_wobbling_press_on_a_slot_moves_no_window` (before, the window moved), `a_wobbling_press_on_a_column_title_moves_the_window`; on the owner's PC, window messages asked the app for Windows' hit test along the title bar: v0.1.0 answered caption from end to end, T26's build answers client over the toggles and +, caption over the titles, min, max and close over Ely's buttons, and a press on close closed the app; the owner, given v0.1.1's build and these controls to click, reported it works on Windows | Pass |
| Ely peeks over the window's top edge from behind it; her face follows the sessions; a press pets her | T46 | `pet::look` tests: the most pressing mark sets her mood, calm blinks, glances and dozes, a finish cheers for 4 s, waiting and errors hold; `pet::faces` tests: every face parses at three times her box and shows its marks; `pet::x11` test under Xvfb: her window stands on the pane, just above the window manager's frame, and unmaps and maps with the main window; on macOS `shot` found her the main window's child, 112 by 124 points on its top edge 140 in, a capture of the screen showed her over the edge, and OSC 7501 reports gave her surprise, worry, eyes down and a cheer; Windows type-checks; Wayland's popup is gpui's own | Pass |
## 7. Sessions panel ## 7. Sessions panel
@@ -124,6 +125,7 @@ What no check reached: no real KWin drew the blur; of Wayland desktops, only Hyp
| Appearance: theme grids, dark theme, font, size, line height, ligatures, cursor and preview, opacity, blur, padding | T17 | T17 review | Pass | | Appearance: theme grids, dark theme, font, size, line height, ligatures, cursor and preview, opacity, blur, padding | T17 | T17 review | Pass |
| On Omarchy, the Omarchy theme first in the grid | T27 | on X11 with Omarchy's theme folder, the Omarchy card leads the grid, chosen, over a preview in its colors | Pass | | On Omarchy, the Omarchy theme first in the grid | T27 | on X11 with Omarchy's theme folder, the Omarchy card leads the grid, chosen, over a preview in its colors | Pass |
| Terminal: scrollback, Option as Alt (macOS), copy on select | T18 | T18 review | Pass | | Terminal: scrollback, Option as Alt (macOS), copy on select | T18 | T18 review | Pass |
| Terminal: Ely, a switch | T46 | on macOS the switch closed her window and saved `"ely": false`, a start with it showed no Ely, and the switch brought her back | Pass |
| Terminal: every shortcut, with a search | T26 | read from the keymap, the bindings that apply in the main window's terminal; on X11 the list showed this platform's keys (Ctrl+Shift+T, Alt+1 and on), and typing "paste" left Edit's Paste, Ctrl+Shift+V | Pass | | Terminal: every shortcut, with a search | T26 | read from the keymap, the bindings that apply in the main window's terminal; on X11 the list showed this platform's keys (Ctrl+Shift+T, Alt+1 and on), and typing "paste" left Edit's Paste, Ctrl+Shift+V | Pass |
## 11. Background blur ## 11. Background blur