T42: a session's status mark moves, as the owner asked after aicss.dev's orbs: 3 by 3 dots in the state's color; work ripples from the centre, progress fills the outer ring clockwise from the top, waiting stays lit and dips twice, error, done and idle draw an X, a check and a ring, then hold; the window draws at the marks' 80 ms steps only while one moves; no review, as the owner asked
This commit is contained in:
9 files changed
+350
-81
No files matched your search
@@ -31,7 +31,7 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
|
||||
## Layout
|
||||
|
||||
- `build.rs`: on Windows, the icon into the executable as resource 1, through `assets/icons/elyterminal.rc`. `.cargo/config.toml`: Windows builds carry the C runtime. `rust-toolchain.toml`: the toolchain rustup installs.
|
||||
- `src/main.rs`: start. `src/lib.rs`: the modules, public so tests and examples reach them. `src/app.rs`: fonts, settings, integration, then the main window, at the size the settings keep, which it keeps as it settles. `src/menus.rs`: the menu bar, the keys of Quit, the font's size and Minimize, and what they do. `src/blur.rs`: what the window shows of the desktop, from the opacity and blur settings, set as they change; `blur/macos` gives gpui's NSVisualEffectView a material that blurs; `blur/wayland` asks a Wayland compositor for blur behind the window's surface through `ext_background_effect_v1`; `blur/x11` names the window's pane in `_KDE_NET_WM_BLUR_BEHIND_REGION`. `src/workspace.rs`: the window's content, Ely's `ColumnShell` with the sessions, terminal and right columns, their toggles, the right column's Git and Files switch, the title, and the sessions' lifecycle. `src/workspace/widths.rs`: the side columns' widths, which a drag on their border sets and the settings keep once it rests. `src/workspace/events.rs`: what a session's terminal reports: title, folder, bell, the state its row shows, the program's exit; and a press on a notification a session posted. `src/sessions.rs`: a session and its row, with the mark of its status and what hover shows of it. `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/settings`: the model, its ranges, and `SettingsStore`, the global that loads, checks and saves `settings.json`; `window` is the settings window, `window/appearance` its Appearance page, `window/terminal` its Terminal page. `tests/fonts.rs` brings its own main: gpui builds its Mac platform only on the main thread.
|
||||
- `src/themes`: built-in terminal themes, the chrome palette each maps to, and `apply`, which dresses the app for the system's appearance; `omarchy`, on Linux, wears Omarchy 4's current theme and follows its switches.
|
||||
- `src/term`: the terminal engine. `pty` starts a program through alacritty's event loop; `tap` wraps the pty so its reader scans OSC 0, 2, 7, 9, 99, 133, 777 and 7501, the title stack's CSI 22 t and 23 t, full resets and the mouse 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: `herdr/unix` finds the client leading the pty's foreground, `herdr/windows` under the session's shell, and asks its named pipe. `unix` starts the program itself, so a failed `chdir` fails the start; Windows uses alacritty's ConPTY. `shell` writes the integration scripts at start and builds each shell's launch. `frame` reads the grid into styled rows, `boxes` turns box drawing, block elements and powerline arrows into rects and polygons (`boxes/outline` strokes curves and diagonals into polygons), `ink` resolves colors, `paint` draws rows, decorations and the cursor, `view` is the entity: pty events, metrics, blink, resize. `zoom` holds the points Bigger and Smaller add to the Size setting, unsaved. `keys` turns key events into bytes, the kitty protocol and, in `keys/legacy`, xterm's encoding, with no gpui types; `input` is the view's key listeners, the bindings that keep Tab and F6 from Ely's focus moves, and its IME handler; `mac` reads the Option side and repeat flag of AppKit's current event, keeps held keys repeating, and hands Command key-ups to the focused view. `mouse` encodes mouse reports, with no gpui types; `links` finds the link at a cell; `pointer` is the view's mouse listeners: selection, reports, wheel, links, copy and paste. `find` is the find box: Ely's `FindWidget`, and Ely's `find_all` over the grid's shown text.
|
||||
@@ -101,5 +101,5 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
|
||||
- 2026-10-17: programs say what they are doing through OSC 7501, revision 0.4 of the Program Status Protocol, which the owner asked for, read as the spec reads it: pairs `key=value` joined by `:`, a malformed pair skipped, an unknown key passed by, and every pair of a known key checked, a clear's too, before the last of each key is kept. A sequence over 4096 bytes, counted from OSC through ST with the controls its body skips, a key over 16, an app over 32, an id outside its grammar or limits (128 bytes, 32 a segment, 8 levels), a title or msg over its limits, not base64, not UTF-8 or holding a control character, and a missing or unknown state each drop it whole; kind counts only with blocked, and progress, 0 to 100, only with working and blocked, each absent when unknown. Each terminal keeps its records by id, the root's empty: a report replaces its record whole, clear drops the record and those under it, or all without an id, and past 256 the record updated longest ago goes. A new prompt drops working, blocked and idle records, as kitty does, and keeps done and error; a full reset, which the tap's scanner sees as `ESC c`, through the controls, DEL and high bytes vte passes by in an escape, drops all, and DECSTR nothing. The query `OSC 7501 ; ?` is answered with every state and kind; the view writes the answer when the session hears the query, ahead of the parser's answers to what came later in the same read, so a DA1 sent after the query, as the spec suggests, comes back second, and one sent before it in the same read comes back after it.
|
||||
- 2026-10-17: programs ask for notifications through OSC 9, 777 and 99 and report progress through OSC 9;4, which the owner asked for. OSC 9 is ConEmu's command where Ghostty reads one, 9;1 to 9;12 each by its own shape, else iTerm2's notification, its text the title; OSC 777 is rxvt's `notify;title;body`, another extension logged and passed by. OSC 99 is kitty's: `k=v` pairs before the payload, a malformed pair passed by; `i`, of `[A-Za-z0-9_-+.]` alone or the sequence drops, gathers chunks while `d=0`, 32 notifications and 16384 bytes of text at most; a payload holds 2048 bytes plain or 4096 encoded at most; plain text is UTF-8 without a control character; `e=1` reads it as base64 group by group, so chunks split before encoding or after join, and it carries any UTF-8, newlines too, checked once the text is whole; a chunk that breaks a rule drops what its notification gathered; metadata holds 1024 bytes at most, and the tap keeps an OSC 99 body to 5124 bytes, room for metadata and a full payload, where other bodies stop at 4096; `p` takes title, body, close, ? and alive, and other payloads pass by; `a`, `c` and `w` are kept for posting, and `o` passes by; a lone body is the title, and one with neither passes by. The query and alive are answered as the notifications below say; an answer names the asker's id, or 0. OSC 9;4's states, remove, set, error, indeterminate and pause, read as Ghostty reads them, the progress clamped to 100, stand as the root record: set and pause working with their progress, indeterminate working without, error an error, remove none; this lasts until a program reports through OSC 7501, and again after a full reset, as the spec asks, so a mapped 9;4 never wipes a real report. The terminal posts the notifications, as below.
|
||||
- 2026-10-17: inside herdr, which passes no OSC 7501 on, herdr's agents are the session's records. The snapshot the follower asks for lists them; one in a state herdr knows, idle, working, blocked or done, is the record `herdr:PANE`, whose colon no OSC 7501 id holds, so a program's record of a name like it, `herdr/build` among them, stays its own; unknown and any state herdr adds later pass by. herdr names no kind and no progress; the app is the agent, the title its name, else the name herdr shows for it, and the message its terminal title less the spinner herdr strips, else the title its metadata sets; herdr's done is idle and not yet seen. A snapshot that lists other agents replaces them whole, so a full reset or a clear that dropped them sees them back at the next; they go when herdr leaves the foreground, and a snapshot that lands after the shell reported a folder is dropped, its agents with it. A change of their states is logged; a herdr whose snapshot lacks `agents` lists none.
|
||||
- 2026-10-17: a session's row shows its status, which the owner asked for: one mark before its title, the most pressing state first. Blocked shows its kind, a shield for permission, a circled question mark for a question, a key for a login and a hand for none, in the theme's warning color; error a circled cross; working Ely's `ProgressRing` at its progress with Ely's loader icon inside, since the ring's track takes the theme's border color, which a selected row's ground matches in Ely Dark, where 0% showed nothing, 24 pixels wide, since at an icon's 16 its stroke filled it, and every mark takes that box, so the title stays put as the state changes; else the loader alone, held still, since a turning spinner would draw the window at every frame for as long as a program works; done a circled check; then a failed last command, a circled alert; then idle, a hollow circle. Among the records in that state, the one reported last speaks. Hover over the mark lists each record, ten at most: its state, its app, its own or its nearest ancestor's as the spec asks, its title and its message, each on one line and cut at 120 characters, with C1 controls, direction controls and invisible formatting taken out, as the spec asks; joiners stay, since scripts and emoji need them. A failed last command's status follows. Hover reads the records as they stand at each frame, so an open one follows a change; it stays while the pointer is over it and scrolls within the window's height, so ten long records all show. A key the user types, a paste too, sees the terminal's done and error records, which then go, as the spec suggests; herdr's stay, since herdr marks them seen. The view notes the state its row shows after each change it hears, not once an output, and tells the workspace of each, and a hidden session whose mark turns to blocked sets its title strong until it is shown, though it went on within the same output, so one output marks as two would. A change of herdr's records draws the view again, so an open hover follows them though the state holds. The view logs each change of that state, and keeps its last command's status.
|
||||
- 2026-10-17: a session's row shows its status, which the owner asked for: one mark before its title, the most pressing state first: 3 by 3 dots in the state's color, which the owner asked for, moving, after the S1 orb of aicss.dev; the dots and their motion are ELYTerminal's own. Working ripples out from the centre to the edges, then the corners, once every 1.2 s; with progress the outer ring fills clockwise from the top, as Ely's `ProgressRing` did, and the next dot pulses, the centre last. Blocked, in the theme's warning color, stays lit and dips twice every 1.6 s, whatever its kind, which hover names. Error, a program's or a failed last command's, draws an X in the danger color; done a check in the success color; idle a dimmer ring in the muted color. A settled shape lights dot by dot, then holds still: one that kept moving would draw the window for as long as it stands. The dots outside a shape stay faint. Every mark steps on one clock, every 80 ms, so the marks move together, and the window draws again at each step only while a mark it shows moves: a working program costs 12.5 frames a second, not one at every refresh of the display, which kept the loader still before. Every mark takes a 24-pixel box, so the title stays put as the state changes; the dots fill 16 of it, an icon's room. Among the records in that state, the one reported last speaks. Hover over the mark lists each record, ten at most: its state, its app, its own or its nearest ancestor's as the spec asks, its title and its message, each on one line and cut at 120 characters, with C1 controls, direction controls and invisible formatting taken out, as the spec asks; joiners stay, since scripts and emoji need them. A failed last command's status follows. Hover reads the records as they stand at each frame, so an open one follows a change; it stays while the pointer is over it and scrolls within the window's height, so ten long records all show. A key the user types, a paste too, sees the terminal's done and error records, which then go, as the spec suggests; herdr's stay, since herdr marks them seen. The view notes the state its row shows after each change it hears, not once an output, and tells the workspace of each, and a hidden session whose mark turns to blocked sets its title strong until it is shown, though it went on within the same output, so one output marks as two would. A change of herdr's records draws the view again, so an open hover follows them though the state holds. The view logs each change of that state, and keeps its last command's status.
|
||||
- 2026-10-17: a session posts system notifications through gpui's, which the owner asked for. At start `set_app_identity` names the app: on Windows that sets the process's AppUserModelID, which the taskbar groups its windows by too, so a pin made before may show apart, and the first notification writes the app's name under `HKCU\Software\Classes\AppUserModelId`, as gpui needs for an app without a package; on macOS gpui posts only from the app bundle, asks for leave at the first, and logs once, under the `gpui_macos` target, that it posts nothing outside one; on Linux it posts over D-Bus through notify-rust. Each terminal view posts its own, in the order its output asks, so OSC 99's answers keep their place before a DA1 sent after them: when its row's mark turns to waiting, done or error, at each turn, as the row's waiting mark is set, the hover's line for the record its body; when a command C started ends, `Command finished`, or `Command exited with N`, so a prompt drawn again, as macOS's bash 3.2 draws one with its D at a resize, posts nothing, and bash 3.2, which sends no C, posts no command's end, as Ghostty's integration, which skips it, posts none; and when a program asks through OSC 9, 777 or 99, its title and body the body, each line shown as text. It posts only while its window is not active or it is not the session shown, which the workspace tells it, one every 3 seconds at most, a later one logged and dropped; the title is the program's title, else the app's name, as the row names the session, which the tap reads in the output's order, so a title set before a notification in the same output names it. The session's own share a tag, so a new one takes the old one's place where the system allows, as an OSC 99 id does; OSC 9, 777 and OSC 99 without an id each take their own; a session keeps its last 64. A press goes to the window whose session posted it: it shows the window and the session, unless OSC 99 said `a=-focus`, and tells a program that asked `a=report`, `OSC 99 ; i=ID ;`; a press whose record went, a twin Linux stacked and pressed before or one past the 64 kept, still shows its session and tells no program. macOS brings the app forward at any press, `a=-focus` or not, and on Wayland gpui asks for the window with a serial the compositor likely refuses, so KWin and Mutter mark it as asking for attention instead. gpui hears of no close on any system, so `c=1` hears `p=close;untracked` once posted, and alive lists the ids posted and not yet pressed, closed or expired. On macOS and Windows a close takes the notification back, and `w` over 0 takes it back once it runs out, its session gone or not, unless a later post holds its tag; gpui on Linux closes and replaces none, so there a close and `w` pass by, logged, a closed one stays alive, and an update posts beside the old one. `o` passes by: each notification posts only while unseen, which is kitty's invisible, so the query answers `a=focus,report:o=invisible:p=title,body,close,?,alive:w=1`, on Linux `a=focus,report:o=invisible:p=title,body,?,alive`, never with the `c` that close events would need. Windows' toasts are silent, as macOS's are; on Linux the notification daemon decides, and reads the body as markup, so its `&`, `<` and `>` go as entities.
|
||||
@@ -80,6 +80,7 @@ Ghostty is the bar for what the terminal does. ELYTerminal owns its engine on `a
|
||||
- A session whose program ends closes its row. The last row closing closes the window.
|
||||
- A bell in a hidden session marks its row.
|
||||
- A row shows its session's status: working, with its progress where known; waiting on the user, for a permission, a question or a login; done; error; and a mark when the last command failed. Hover shows each record's program, title and message. A hidden session that starts waiting marks its row until it is shown.
|
||||
- The mark is 3 by 3 dots that move: work ripples, or fills by its progress; waiting dips; a settled state draws its shape, then holds still.
|
||||
- System notifications on macOS, Windows and Linux: a session that starts waiting, finishes or fails, and a program's OSC 9, 777 or 99 notification, post one while the window is not active or the session is hidden, one a session every 3 seconds at most; it names the session, and a press on it shows the window and the session. On macOS only the app bundle posts them.
|
||||
|
||||
## 8. Right panel: Files
|
||||
|
||||
@@ -43,7 +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; 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; 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.
|
||||
- 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; 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`.
|
||||
- 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.
|
||||
- A button at each end of the title bar hides or shows its side column, and a drag on the border between a side column and the terminal resizes it; the window opens as it was left, at the size and widths it was left at. On Windows and Linux a gear beside the right one opens Settings.
|
||||
|
||||
@@ -102,3 +102,11 @@ Progress truth. Top to bottom, one at a time. A task is ticked only after its co
|
||||
## 21. Release
|
||||
|
||||
- [x] T41 Release v0.1.3 with T32 to T40: version 0.1.3; the macOS disk image, the Windows zip and the Linux tarball, each from its script; the notes list what changed.
|
||||
|
||||
## 22. Status marks that move
|
||||
|
||||
- [x] T42 The status marks move, which the owner asked for after aicss.dev's orbs: 3 by 3 dots in the state's color; work ripples, or fills by its progress, waiting dips, a settled state draws its shape, then holds; the window draws again only at the marks' steps while one moves.
|
||||
|
||||
## 23. Release
|
||||
|
||||
- [ ] T43 v0.1.3 rebuilt with T42, which the owner asked for in place of a v0.1.4: the version stays 0.1.3; the macOS disk image, the Windows zip and the Linux tarball, each from its script, replace the old ones on the release; the tag moves to their commit; the notes say what changed.
|
||||
+91
-75
@@ -1,19 +1,25 @@
|
||||
use std::path::{MAIN_SEPARATOR, Path, PathBuf};
|
||||
use std::{
|
||||
path::{MAIN_SEPARATOR, Path, PathBuf},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use ely_gpui_component::{
|
||||
buttons::{ButtonVariant, IconButton},
|
||||
lists::ListItem,
|
||||
motion::ProgressRing,
|
||||
primitives::{Icon, IconName, Tooltip},
|
||||
theme::{ActiveTheme, ControlSize, IconSize},
|
||||
};
|
||||
use gpui::{AnyElement, App, Entity, Hsla, SharedString, Subscription, div, prelude::*, rems};
|
||||
use gpui::{AnyElement, App, Entity, SharedString, Subscription, div, prelude::*, rems};
|
||||
|
||||
use crate::{
|
||||
term::{Kind, Record, Records, State, TerminalView},
|
||||
workspace::{TITLE, Workspace},
|
||||
};
|
||||
|
||||
pub mod orb;
|
||||
|
||||
use orb::Look;
|
||||
|
||||
/// A program on its own terminal, as the sessions column lists it.
|
||||
pub struct Session {
|
||||
pub terminal: Entity<TerminalView>,
|
||||
@@ -27,9 +33,25 @@ pub struct Session {
|
||||
pub bell: bool,
|
||||
/// It started waiting on the user while hidden.
|
||||
pub waiting: bool,
|
||||
/// The look its mark shows, and since when.
|
||||
pub look: Option<(Look, Instant)>,
|
||||
pub _events: Subscription,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// Notes the look its mark takes `now`; true while the mark moves.
|
||||
pub fn note(&mut self, now: Instant, cx: &App) -> bool {
|
||||
let view = self.terminal.read(cx);
|
||||
let failed = view.ended().is_some_and(|code| code != 0);
|
||||
let shows = look(view.records().shown().map(|(_, record)| record), failed);
|
||||
if self.look.map(|(shown, _)| shown) != shows {
|
||||
self.look = shows.map(|shows| (shows, now));
|
||||
}
|
||||
self.look
|
||||
.is_some_and(|(look, since)| orb::moves(look, now.saturating_duration_since(since)))
|
||||
}
|
||||
}
|
||||
|
||||
/// The most records hover lists, and the most characters it shows of each part.
|
||||
const LISTED: usize = 10;
|
||||
const SHORT: usize = 120;
|
||||
@@ -71,6 +93,7 @@ pub fn row(
|
||||
session: &Session,
|
||||
active: bool,
|
||||
home: Option<&Path>,
|
||||
now: Instant,
|
||||
cx: &App,
|
||||
) -> ListItem {
|
||||
let terminal = session.terminal.read(cx);
|
||||
@@ -96,7 +119,7 @@ pub fn row(
|
||||
close.update(cx, |this, cx| this.close(index, window, cx))
|
||||
}),
|
||||
);
|
||||
let mark = mark(&session.terminal, cx);
|
||||
let mark = mark(session, now, cx);
|
||||
let bell = session
|
||||
.bell
|
||||
.then(|| Icon::new(IconName::Bell).color(cx.theme().colors.warning));
|
||||
@@ -113,79 +136,36 @@ pub fn row(
|
||||
)
|
||||
}
|
||||
|
||||
/// The mark of what the session's records say, else of its last command's failure; hover lists them.
|
||||
fn mark(terminal: &Entity<TerminalView>, cx: &App) -> Option<AnyElement> {
|
||||
/// What a mark shows: the most pressing record's state, else a failed last command.
|
||||
pub fn look(record: Option<&Record>, failed: bool) -> Option<Look> {
|
||||
Some(
|
||||
match (record.map(|record| (record.state, record.progress)), failed) {
|
||||
(Some((State::Blocked, _)), _) => Look::Waiting,
|
||||
(Some((State::Error, _)), _) => Look::Error,
|
||||
(Some((State::Working, progress)), _) => Look::Working(progress),
|
||||
(Some((State::Done, _)), _) => Look::Done,
|
||||
(_, true) => Look::Error,
|
||||
(Some((State::Idle, _)), false) => Look::Idle,
|
||||
(None, false) => return None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// The session's mark: its look's dots in its state's color; hover lists the records.
|
||||
fn mark(session: &Session, now: Instant, cx: &App) -> Option<AnyElement> {
|
||||
let (look, since) = session.look?;
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let view = terminal.read(cx);
|
||||
let (pid, records) = (view.pid() as usize, view.records());
|
||||
let icon = |name: IconName, color: Hsla| Icon::new(name).color(color).into_any_element();
|
||||
let failed = view.ended().filter(|&code| code != 0);
|
||||
let mark = match (records.shown().map(|(_, record)| record), failed) {
|
||||
(
|
||||
Some(Record {
|
||||
state: State::Blocked,
|
||||
kind,
|
||||
..
|
||||
}),
|
||||
_,
|
||||
) => {
|
||||
let name = match kind {
|
||||
Some(Kind::Permission) => IconName::Shield,
|
||||
Some(Kind::Question) => IconName::CircleHelp,
|
||||
Some(Kind::Auth) => IconName::Key,
|
||||
None => IconName::Hand,
|
||||
};
|
||||
icon(name, colors.warning)
|
||||
}
|
||||
(
|
||||
Some(Record {
|
||||
state: State::Error,
|
||||
..
|
||||
}),
|
||||
_,
|
||||
) => icon(IconName::CircleX, colors.danger),
|
||||
(
|
||||
Some(Record {
|
||||
state: State::Working,
|
||||
progress: Some(value),
|
||||
..
|
||||
}),
|
||||
_,
|
||||
) => ProgressRing::new(("session-progress", pid), f32::from(*value) / 100.0)
|
||||
.size(theme.icon_size(IconSize::Xl))
|
||||
// The track takes the border's color, which a selected row's ground may match.
|
||||
.inside(
|
||||
Icon::new(IconName::LoaderCircle)
|
||||
.size(IconSize::Xs)
|
||||
.color(colors.info),
|
||||
)
|
||||
.into_any_element(),
|
||||
// A turning spinner would draw the window at every frame while the program works.
|
||||
(
|
||||
Some(Record {
|
||||
state: State::Working,
|
||||
..
|
||||
}),
|
||||
_,
|
||||
) => icon(IconName::LoaderCircle, colors.info),
|
||||
(
|
||||
Some(Record {
|
||||
state: State::Done, ..
|
||||
}),
|
||||
_,
|
||||
) => icon(IconName::CircleCheck, colors.success),
|
||||
(_, Some(_)) => icon(IconName::CircleAlert, colors.danger),
|
||||
(
|
||||
Some(Record {
|
||||
state: State::Idle, ..
|
||||
}),
|
||||
None,
|
||||
) => icon(IconName::Circle, colors.fg_muted),
|
||||
(None, None) => return None,
|
||||
let color = match look {
|
||||
Look::Working(_) => colors.info,
|
||||
Look::Waiting => colors.warning,
|
||||
Look::Error => colors.danger,
|
||||
Look::Done => colors.success,
|
||||
Look::Idle => colors.fg_muted,
|
||||
};
|
||||
let pid = session.terminal.read(cx).pid() as usize;
|
||||
// Hover reads the records as they stand, so an open one follows them, and scrolls within the window.
|
||||
let watched = terminal.downgrade();
|
||||
let watched = session.terminal.downgrade();
|
||||
let tip = Tooltip::rich(move |window, cx| {
|
||||
let lines = watched.upgrade().map_or_else(Vec::new, |terminal| {
|
||||
let view = terminal.read(cx);
|
||||
@@ -201,7 +181,7 @@ fn mark(terminal: &Entity<TerminalView>, cx: &App) -> Option<AnyElement> {
|
||||
.children(lines)
|
||||
.into_any_element()
|
||||
});
|
||||
// Every mark takes the ring's box, so the title stays put as the state changes.
|
||||
// Every mark takes a 24-pixel box, so the title stays put as the state changes.
|
||||
Some(
|
||||
div()
|
||||
.id(("session-status", pid))
|
||||
@@ -210,7 +190,7 @@ fn mark(terminal: &Entity<TerminalView>, cx: &App) -> Option<AnyElement> {
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.hoverable_tooltip(tip)
|
||||
.child(mark)
|
||||
.child(orb::lattice(look, color, now, since))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
@@ -332,6 +312,42 @@ mod tests {
|
||||
assert_eq!(after_close(1, 2, 3), 1, "one after it went");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_mark_shows_the_most_pressing_look() {
|
||||
let record = |state, progress| Record {
|
||||
state,
|
||||
kind: None,
|
||||
progress,
|
||||
app: None,
|
||||
title: None,
|
||||
msg: None,
|
||||
};
|
||||
let waiting = look(Some(&record(State::Blocked, None)), true);
|
||||
assert_eq!(
|
||||
waiting,
|
||||
Some(Look::Waiting),
|
||||
"waiting outranks a failed command"
|
||||
);
|
||||
let working = look(Some(&record(State::Working, Some(40))), false);
|
||||
assert_eq!(working, Some(Look::Working(Some(40))));
|
||||
let idle = record(State::Idle, None);
|
||||
assert_eq!(
|
||||
look(Some(&idle), true),
|
||||
Some(Look::Error),
|
||||
"a failed command outranks idle"
|
||||
);
|
||||
assert_eq!(look(Some(&idle), false), Some(Look::Idle));
|
||||
assert_eq!(
|
||||
look(Some(&record(State::Done, None)), true),
|
||||
Some(Look::Done)
|
||||
);
|
||||
assert_eq!(
|
||||
look(Some(&record(State::Error, None)), false),
|
||||
Some(Look::Error)
|
||||
);
|
||||
assert_eq!(look(None, false), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_shows_a_program_s_text_as_text() {
|
||||
assert_eq!(plain("evil\u{202e}txt.exe"), "eviltxt.exe");
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
//! A session's status mark: 3 by 3 dots in its state's color, every mark stepping on one clock.
|
||||
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
f64::consts::TAU,
|
||||
sync::OnceLock,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use gpui::{Div, Hsla, ParentElement, Styled, div};
|
||||
|
||||
/// The dots' beat: while a mark moves, the window draws at each step, not at every frame.
|
||||
pub const STEP: Duration = Duration::from_millis(80);
|
||||
|
||||
/// What a mark shows.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Look {
|
||||
/// A ripple from the centre; with progress, the outer ring fills clockwise from the top.
|
||||
Working(Option<u8>),
|
||||
/// Lit, with two quick dips.
|
||||
Waiting,
|
||||
/// A program's error, or a failed last command.
|
||||
Error,
|
||||
Done,
|
||||
Idle,
|
||||
}
|
||||
|
||||
/// One ripple, in seconds, and how far a dot one dot out from the centre lags it.
|
||||
const RIPPLE: f64 = 1.2;
|
||||
const LAG: f64 = 0.3;
|
||||
/// Steps in one round of a waiting mark's dips.
|
||||
const BLINK: u64 = 20;
|
||||
/// A working dot at its lowest, a waiting mark in a dip, and a dot outside a settled shape.
|
||||
const DIM: f32 = 0.35;
|
||||
const DIP: f32 = 0.25;
|
||||
const FAINT: f32 = 0.12;
|
||||
/// Steps a settled shape's dot takes to light, and between one dot and the next.
|
||||
const FADE: u64 = 2;
|
||||
const STAGGER: u64 = 1;
|
||||
/// The outer ring clockwise from the top, then the centre: the order progress fills.
|
||||
const FILL: [usize; 9] = [1, 2, 5, 8, 7, 6, 3, 0, 4];
|
||||
|
||||
/// A settled look's dots in the order they light, and how bright they end; none for a look that keeps moving.
|
||||
fn shape(look: Look) -> Option<(&'static [usize], f32)> {
|
||||
match look {
|
||||
Look::Working(_) | Look::Waiting => None,
|
||||
Look::Error => Some((&[0, 4, 8, 2, 6], 1.0)),
|
||||
Look::Done => Some((&[3, 7, 5, 2], 1.0)),
|
||||
Look::Idle => Some((&FILL[..8], 0.6)),
|
||||
}
|
||||
}
|
||||
|
||||
fn steps(time: Duration) -> u64 {
|
||||
(time.as_millis() / STEP.as_millis()) as u64
|
||||
}
|
||||
|
||||
/// A dot's brightness `t` seconds into its ripple.
|
||||
fn crest(t: f64) -> f32 {
|
||||
let wave = 0.5 + 0.5 * (TAU * t / RIPPLE).cos();
|
||||
DIM + (1.0 - DIM) * wave as f32
|
||||
}
|
||||
|
||||
/// Each dot's opacity, row by row, `clock` on the marks' clock and `held` after the look began.
|
||||
pub fn dots(look: Look, clock: Duration, held: Duration) -> [f32; 9] {
|
||||
let t = steps(clock) as f64 * STEP.as_secs_f64();
|
||||
let mut levels = [DIM; 9];
|
||||
match look {
|
||||
Look::Working(None) => {
|
||||
for (dot, level) in levels.iter_mut().enumerate() {
|
||||
let (x, y) = ((dot % 3) as f64 - 1.0, (dot / 3) as f64 - 1.0);
|
||||
*level = crest(t - x.hypot(y) * LAG);
|
||||
}
|
||||
}
|
||||
Look::Working(Some(progress)) => {
|
||||
let filled = (usize::from(progress) * FILL.len() / 100).min(FILL.len() - 1);
|
||||
for (rank, &dot) in FILL.iter().enumerate() {
|
||||
levels[dot] = match rank.cmp(&filled) {
|
||||
Ordering::Less => 1.0,
|
||||
Ordering::Equal => crest(t),
|
||||
Ordering::Greater => DIM,
|
||||
};
|
||||
}
|
||||
}
|
||||
Look::Waiting => {
|
||||
let dip = matches!(steps(clock) % BLINK, 0 | 1 | 4 | 5);
|
||||
levels = [if dip { DIP } else { 1.0 }; 9];
|
||||
}
|
||||
settled => {
|
||||
let (shape, bright) = shape(settled).expect("a settled look has a shape");
|
||||
levels = [FAINT; 9];
|
||||
let held = steps(held);
|
||||
for (rank, &dot) in shape.iter().enumerate() {
|
||||
let lit = held.saturating_sub(rank as u64 * STAGGER).min(FADE) as f32 / FADE as f32;
|
||||
levels[dot] = bright * lit + FAINT * (1.0 - lit);
|
||||
}
|
||||
}
|
||||
}
|
||||
levels
|
||||
}
|
||||
|
||||
/// Whether a mark still changes `held` after its look began: work and waiting always, a settled shape until it is drawn.
|
||||
pub fn moves(look: Look, held: Duration) -> bool {
|
||||
shape(look).is_none_or(|(shape, _)| steps(held) < (shape.len() as u64 - 1) * STAGGER + FADE)
|
||||
}
|
||||
|
||||
/// Time on the clock every mark steps to, so marks move together.
|
||||
fn clock(now: Instant) -> Duration {
|
||||
static START: OnceLock<Instant> = OnceLock::new();
|
||||
now.saturating_duration_since(*START.get_or_init(|| now))
|
||||
}
|
||||
|
||||
/// How long until the clock's next step, a little past it.
|
||||
pub fn wait(now: Instant) -> Duration {
|
||||
let into = clock(now).as_nanos() % STEP.as_nanos();
|
||||
STEP - Duration::from_nanos(into as u64) + Duration::from_millis(2)
|
||||
}
|
||||
|
||||
/// The mark's dots in `color`, `now`, its look shown since `since`.
|
||||
pub fn lattice(look: Look, color: Hsla, now: Instant, since: Instant) -> Div {
|
||||
let levels = dots(look, clock(now), now.saturating_duration_since(since));
|
||||
div()
|
||||
.grid()
|
||||
.grid_cols(3)
|
||||
.gap_0p5()
|
||||
.children(levels.map(|level| div().size_1().rounded_full().bg(color.opacity(level))))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn at(steps: u32) -> Duration {
|
||||
STEP * steps
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn work_ripples_from_the_centre_to_the_edges_then_the_corners() {
|
||||
let levels = |steps| dots(Look::Working(None), at(steps), Duration::ZERO);
|
||||
let peak = |dot: usize| {
|
||||
(0..15)
|
||||
.max_by(|&a, &b| levels(a)[dot].total_cmp(&levels(b)[dot]))
|
||||
.expect("steps")
|
||||
};
|
||||
let (centre, edge, corner) = (peak(4), peak(1), peak(0));
|
||||
assert_eq!(centre, 0);
|
||||
assert!(centre < edge && edge < corner, "{centre} {edge} {corner}");
|
||||
assert_eq!(peak(3), edge, "every edge together");
|
||||
assert_eq!(peak(8), corner, "every corner together");
|
||||
let again = levels(15);
|
||||
assert!(
|
||||
levels(0)
|
||||
.iter()
|
||||
.zip(again)
|
||||
.all(|(a, b)| (a - b).abs() < 1e-4),
|
||||
"one ripple every 15 steps"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn progress_fills_the_ring_clockwise_from_the_top_then_the_centre() {
|
||||
let lit = |progress, steps| dots(Look::Working(Some(progress)), at(steps), Duration::ZERO);
|
||||
let half = lit(50, 9);
|
||||
assert!([1, 2, 5, 8].iter().all(|&dot| half[dot] == 1.0), "{half:?}");
|
||||
assert!([6, 3, 0, 4].iter().all(|&dot| half[dot] == DIM), "{half:?}");
|
||||
assert!(lit(50, 0)[7] > half[7], "the next dot pulses");
|
||||
let full = lit(100, 9);
|
||||
assert!(FILL[..8].iter().all(|&dot| full[dot] == 1.0), "{full:?}");
|
||||
assert!(lit(100, 0)[4] > full[4], "then the centre pulses");
|
||||
assert!(lit(0, 0)[1] > lit(0, 9)[1], "at 0% the top pulses");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn waiting_stays_lit_and_dips_twice() {
|
||||
let blink = |steps| dots(Look::Waiting, at(steps), Duration::ZERO);
|
||||
let dips: Vec<u32> = (0..20).filter(|&steps| blink(steps)[0] == DIP).collect();
|
||||
assert_eq!(dips, [0, 1, 4, 5]);
|
||||
assert!((0..20).all(|steps| blink(steps).iter().all(|&level| level == blink(steps)[0])));
|
||||
assert_eq!(blink(2), [1.0; 9]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_settled_look_draws_its_shape_dot_by_dot_then_holds() {
|
||||
let done = |steps| dots(Look::Done, at(1000), at(steps));
|
||||
assert_eq!(done(0), [FAINT; 9], "nothing lit at first");
|
||||
assert!(
|
||||
done(1)[3] > FAINT && done(1)[2] == FAINT,
|
||||
"the check draws from its left end"
|
||||
);
|
||||
let check = [3, 7, 5, 2];
|
||||
let drawn = done(5);
|
||||
assert!((0..9).all(|dot| drawn[dot] == if check.contains(&dot) { 1.0 } else { FAINT }));
|
||||
assert_eq!(done(500), drawn, "then it holds still");
|
||||
assert!(moves(Look::Done, at(4)) && !moves(Look::Done, at(5)));
|
||||
let idle = dots(Look::Idle, Duration::ZERO, at(9));
|
||||
assert_eq!(idle[4], FAINT, "idle is a ring");
|
||||
assert!(FILL[..8].iter().all(|&dot| idle[dot] == 0.6));
|
||||
assert!(!moves(Look::Idle, at(9)) && moves(Look::Idle, at(8)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn work_and_waiting_never_stop() {
|
||||
for look in [Look::Working(None), Look::Working(Some(40)), Look::Waiting] {
|
||||
assert!(moves(look, at(1_000_000)), "{look:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
-3
@@ -1,3 +1,5 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use ely_gpui_component::{
|
||||
buttons::{ButtonVariant, IconButton, SegmentedControl},
|
||||
lists::List,
|
||||
@@ -9,7 +11,7 @@ use ely_gpui_component::{
|
||||
use gpui::{
|
||||
Action, App, AppContext, Context, Entity, FocusHandle, Focusable, InteractiveElement,
|
||||
IntoElement, KeyBinding, ParentElement, Render, SharedString, StatefulInteractiveElement,
|
||||
StyleRefinement, Styled, Subscription, Window, actions, div, prelude::FluentBuilder,
|
||||
StyleRefinement, Styled, Subscription, Task, Window, actions, div, prelude::FluentBuilder,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -17,7 +19,7 @@ use crate::{
|
||||
files::Files,
|
||||
git::panel::{Acted, GitPanel},
|
||||
menus,
|
||||
sessions::{self, Session},
|
||||
sessions::{self, Session, orb},
|
||||
settings::{PanelTab, Settings, SettingsStore, StartIn, window::OpenSettings},
|
||||
term::{
|
||||
Events, Launch, Pty, TerminalView, config,
|
||||
@@ -95,6 +97,8 @@ pub struct Workspace {
|
||||
background: Blur,
|
||||
/// Column widths a drag shows before the settings hold them.
|
||||
dragged: Dragged,
|
||||
/// Draws the window at each step of the status marks while one shown moves.
|
||||
ticking: Option<Task<()>>,
|
||||
_settings: Subscription,
|
||||
_acted: Subscription,
|
||||
}
|
||||
@@ -122,6 +126,7 @@ impl Workspace {
|
||||
git,
|
||||
background: Blur::new(window, SettingsStore::get(cx)),
|
||||
dragged: Dragged::default(),
|
||||
ticking: None,
|
||||
_settings: cx.observe_global_in::<SettingsStore>(window, |this, window, cx| {
|
||||
this.background.follow(window, SettingsStore::get(cx));
|
||||
cx.notify();
|
||||
@@ -155,10 +160,36 @@ impl Workspace {
|
||||
reports: launch.reports,
|
||||
bell: false,
|
||||
waiting: false,
|
||||
look: None,
|
||||
_events: events,
|
||||
});
|
||||
}
|
||||
|
||||
/// Notes each session's mark `now`, and draws the window at each step while one shown moves.
|
||||
fn tick(&mut self, now: Instant, cx: &mut Context<Self>) {
|
||||
let mut moving = false;
|
||||
for session in &mut self.sessions {
|
||||
moving |= session.note(now, cx);
|
||||
}
|
||||
let moving = moving && SettingsStore::get(cx).show_sessions;
|
||||
if moving == self.ticking.is_some() {
|
||||
return;
|
||||
}
|
||||
log::debug!("app: status marks {}", if moving { "move" } else { "rest" });
|
||||
self.ticking = moving.then(|| {
|
||||
cx.spawn(async move |this, cx| {
|
||||
loop {
|
||||
cx.background_executor()
|
||||
.timer(orb::wait(Instant::now()))
|
||||
.await;
|
||||
if this.update(cx, |_, cx| cx.notify()).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/// A session of the shell the settings name, in home or the shown session's folder.
|
||||
fn new_session(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(home) = dirs::home_dir() else {
|
||||
@@ -307,6 +338,8 @@ fn column_toggle(
|
||||
impl Render for Workspace {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.background.fit(window);
|
||||
let now = Instant::now();
|
||||
self.tick(now, cx);
|
||||
let settings = SettingsStore::get(cx);
|
||||
let workspace = cx.entity();
|
||||
let tip =
|
||||
@@ -350,7 +383,7 @@ impl Render for Workspace {
|
||||
let home = dirs::home_dir();
|
||||
let rows = self.sessions.iter().enumerate().map(|(index, session)| {
|
||||
let active = index == self.active;
|
||||
sessions::row(&workspace, index, session, active, home.as_deref(), cx)
|
||||
sessions::row(&workspace, index, session, active, home.as_deref(), now, cx)
|
||||
});
|
||||
let list = List::new().children(rows);
|
||||
let new = IconButton::new("new-session", IconName::Plus)
|
||||
|
||||
@@ -58,6 +58,7 @@ What no check reached: no real KWin drew the blur; of Wayland desktops, only Hyp
|
||||
| Inside herdr, the folder of herdr's focused pane, read from its socket, on macOS, Linux and Windows | T29, T32 | `term::herdr` tests: the socket found as herdr 0.9 finds it, only the terminal client followed and a remote one refused, the snapshot read, a socket round trip, a process's path, arguments and variables; `the_foreground_is_the_program_s_own_group`; on X11 with herdr 0.9.5 run in the session: its start showed the focused pane's folder, a cd in the pane moved the panel, the focus moved to a pane in `/etc` and back moved it each time, and the client's end gave the panel back to bash's report, then `exec herdr` from that bash, with `HERDR_SOCKET_PATH` relative to its folder, was followed through the socket there; on macOS under `shot`, herdr 0.9.5 as the program and again by `exec` from `sh` with a relative socket, its pane's cd moved the panel; on Windows, `term::herdr` tests on the CI runner: the folder under `APPDATA` and `USERPROFILE`, the `herdr.exe` nearest under the shell, this test's name, arguments and variables read through the live process list and `sysinfo`, a round trip over a named pipe; no real herdr has run on Windows yet | Partial |
|
||||
| Inside herdr, herdr's agent states are the session's records | T38 | `term::herdr` tests: each agent in a state herdr knows is its pane's record, its app, title and message from herdr's fields, unknown and new states passed by, and a snapshot without agents lists none. `term::status`: herdr's agents replace their own records alone, a program's `herdr/build` kept through an empty snapshot, a full one and herdr's leaving, since a program's id ends at a colon; the same agents change nothing, and the next snapshot puts them back after a full reset | Pass |
|
||||
| A row shows its session's status, hover each record; a hidden session that starts waiting marks its row | T39 | `term::status` tests: blocked, then error, working, done and idle show, the record reported last in its state speaks; an app comes from the nearest ancestor that names one; typing sees done and error, herdr's aside; each change of the state a row shows is noted, a wait after work in one output among them. `sessions` tests: hover lists each record's state, app, title and message, ten at most, then a failed command's status; a program's text shows on one line without direction controls or invisible formatting, joiners kept, cut at 120 characters. In the app on macOS, `shot` showed each mark: the ring at 40%, the still loader, a shield over three records whose hover listed each, the root's app inherited, a check, a cross, a hollow circle, an alert after `133;D;1`, a hidden session's question mark with its title strong, and a done record gone after a typed key; after review, a hidden session that waited, worked and waited again in one output set its title strong, an open hover followed a record replaced under it and work from 20% to 80%, ten long records scrolled to the last within the window, and a selected row at 0% in Ely Dark showed its loader; after round 2, herdr's records replaced in the same state showed at once in an open hover, and a hidden session that waited and worked again in one output set its title strong | Pass |
|
||||
| A row's mark moves: work ripples from the centre, progress fills the outer ring clockwise from the top, waiting stays lit and dips, a settled state draws its shape and holds; the window draws at the marks' steps only while one moves | T42 | `sessions::orb` tests: the ripple peaks at the centre, then every edge, then every corner, once every 15 steps; at 50% the first four ring dots are lit, the next pulses and the rest stay dim, and at 100% the centre pulses; waiting dips on the 1st, 2nd, 5th and 6th of 20 steps, every dot alike; a done check draws from its left end and holds after 5 steps, idle is a ring; work and waiting never stop. `sessions` test: the mark takes the most pressing look, a failed command as an error. In the app on macOS, at 70% glass in Ely Dark, the recorder's stills showed each look in one column: the ripple, 40% filled, waiting lit, a check, two crosses and a ring; two stills 0.4 s apart showed the ripple at two steps | Pass |
|
||||
| System notifications on macOS, Windows and Linux: a session that starts waiting, finishes or fails, and OSC 9, 777 and 99, while the window is inactive or the session hidden, one a session every 3 s; a press shows the window and session | T40 | `term::reports` tests: a session posts once in 3 s, its own under one tag; a program's notifications go by their id and live until pressed, closed or expired, an expiry taking only its own post and none a later post holds; a session keeps its last 64; the text shows as text, and as markup's text on Linux; a session posts unless it is the one shown in an active window; waiting, done and error post, work and rest do not; a tag names the session that posted it, by itself, an id or a count. `term::titles`: OSC 0 and 2 set the title as vte splits it, 15 parts past the number at most, and as alacritty joins it; CSI 22 t and 23 t save it and bring it back; a full reset empties both. `term::tap`: the scanner finds CSI 22 t and 23 t as vte reads them; a title's body holds 65536 bytes, another's 4096. `term::pty`: titles, notifications and full resets reach the session in the output's order, one write and a write a sequence alike, a title of 5000 bytes and one of 17 parts among them. `term::notify`: the query, on Linux without close and `w`, alive, untracked and activation answers. In the app on macOS, outside a bundle, where gpui posts nothing and the log names each post: an OSC 9 posted and an OSC 777 within 3 s was dropped; an OSC 99 with `c=1` and `w=800` posted, its program heard `p=close;untracked`, alive listed it, and once it expired alive listed none; a closed one left alive empty; the query's answer came back; a command's end without its C posted nothing, and a failed one after its C posted; a notice after a title set, or after a full reset, in the same write named the session as split writes do, as did one after a full reset and 2 KB to 32 KB more of output, and a title of 17 parts or of 4100 bytes named the notice as it named the row, one write or two; a notice, alive and DA1 in one write came back as alive naming it, then DA1's answer; a notice set to expire whose session ended expired after it; a hidden session that waited and worked again in one output posted its wait; and a notice sent while the window was active and the session shown posted nothing, the next, once the window lost focus, posted. No system showed one yet: on macOS gpui posts only from the app bundle, which asks the owner's leave first, and Windows and Linux did not run it; a press needs the system's notification center, so the owner posts and presses one on each | Partial |
|
||||
| Colors, styles, five underlines, wide characters, combining marks | T06 | `term::frame`, `term::ink`, `term::paint` tests | Pass |
|
||||
| Box drawing, blocks and powerline shapes as geometry | T07 | `term::boxes` tests | Pass |
|
||||
|
||||
@@ -209,3 +209,7 @@ Each task gets at most three codex rounds. Each round: the commit, the verdict,
|
||||
## T41
|
||||
|
||||
- Round 1, 7569f00 with release v0.1.3: PASS. The reviewer compared `Cargo.lock` with its parent, where of 844 packages only elyterminal moved, to 0.1.3; matched the 127 files of the tag's source archive to 7569f00 byte for byte; downloaded the three files anonymously against their SHA-256s; checked the disk image's stapled notarization and its app's Developer ID signature, hardened runtime and version 0.1.3, the zip's icon and manifest against their sources byte for byte and its imports, no C runtime DLL among them, and the tarball's three files and glibc 2.35; and matched the notes to T32 to T40. The app was not run, since its first notification asks the owner's leave. CI run 1156 on 7569f00 passed on macOS, Ubuntu and Windows before the release was published; Release run 1157 built the notarized disk image.
|
||||
|
||||
## T42
|
||||
|
||||
- No review: the owner waived it for this change.
|
||||
Reference in new issue
Block a user