T35: review round 3: a program's link target is kept whole, as the scanner keeps it, so a prompt inside a long link is numbered and gives the link back plain at its end, and the tap reads a 517th of its room to hold the growth; an ESC that ends an OSC the parser has goes on at once, so controls after it run where vte runs them
CI / platforms (ubuntu-24.04) (push) Successful in 1m15s
CI / platforms (windows-latest) (push) Successful in 1m45s
CI / macos (push) Successful in 11m49s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-17 00:26:34 -04:00
1 parent e2bb615a19
commit 11ec0effa0
5 files changed
+104 -49

No files matched your search

+1 -1
View File
@@ -97,4 +97,4 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- 2026-10-16: the main window opens at the size it was left at, which the owner asked for on macOS, Windows and Linux: `window_width` and `window_height` in the settings hold the size gpui's `window_bounds` gives 0.5 s after the bounds settle, saved through `SettingsStore::update`, which writes only a change, so a drag saves once and a move saves nothing. A maximized, full-screen or hidden window keeps the size from before; its state and size are read once they settle, since X11 marks a window maximized after its new size, gpui's X11 window then gives a maximized window's own size, and it calls a maximized window unmaximized while minimized. A window that shows again, uncovered, restored or back on its screen, is read again, so a size it took just before it hid is kept; a size it takes in the half second before it closes, or before it hides until the app quits, is lost. The window is no smaller than 100 by 100, the least the settings keep, or a smaller size the platform allows could not be kept. The window opens centered at whole points, since macOS grows a window by a point on a side whose edge falls on a half point; its place is not kept, and a kept side past the primary display's visible area shrinks to it, logged. A size outside 100 to 16384 stops the start, as any value out of range does.
- 2026-10-16: the sessions and right columns resize by a drag on their border toward the terminal, which the owner asked for on macOS, Windows and Linux. Ely's `ShellColumn::resizable`, added to Ely for this, puts Ely's `resize_handle` on the border toward the filling column, the sessions column's right and the right column's left: the handle draws that border, a filled hairline, since gpui paints a border no wider than half its box, and the line it drew at first showed half a pixel on macOS and none on X11; it occludes the header under it, so a press there drags the border, not the window, on Windows too, where an unoccluded drag region takes the press as the caption. A drag sets off from the wider of the width drawn and the one kept: a header's parts may hold a column wider, and a window too narrow for both widths squeezes them, where a drag from the squeezed width jumped back against the pointer; a drag that has not moved sideways changes nothing, and one that comes back where it set off gives the width it had. A set width in Ely became a flex basis, so a column holds its header's own parts: at a set width under them, Windows' caption buttons ran out of the window. The widths, 160 to 960 pixels, show at once and are saved in `sessions_width` and `panel_width` through `SettingsStore::update` 0.5 s after a drag rests, as the window's size is; a drag in the half second before the window closes is lost, and a save the store refuses puts the columns back. A window too narrow for both yields the terminal its header's width, as Ely's set widths give way.
- 2026-10-16: prompts and commands carry OSC 133 marks, which the owner asked for, read as Ghostty reads them: A or N starts a prompt, B or I ends it, C starts a command's output, and D ends the command with its status, D's first option, none when that is no number. The tap reads any program's marks before the parser, so it never sees the cursor: A's fresh line and L, which need it, are not done, and P adds nothing to A. Each session keeps the status its last command ended with. The integration marks what the shell leaves unmarked, and the user's prompt still sees the command's status. zsh: precmd reads `$?` and sends D after a command ran, preexec sends C, and a last precmd hook wraps PS1 in A and B under `prompt_percent`, which the first prompt says while it is off; it moves to the end of `precmd_functions` at each prompt, since a theme that sets PS1 in a hook of its own loads after it, and at the first prompt `sched +0` wraps it once more, since zsh runs due events after every precmd hook; that call returns the command's status, which the prompt shows, but it would reset `$pipestatus`, so it serves the first prompt alone. bash: PROMPT_COMMAND's first entry reads `$?`, which each entry resets, and its last wraps PS1. From bash 4.4 PS0 sends C and its subscript notes that a command ran, since bash prints no PS0 for an empty line, and the first entry sends D for it; both need `promptvars`, on by default. macOS's bash 3.2 has no PS0 and nothing else between reading a line and running it: its DEBUG trap runs for a key binding's command (`bind -x`) as for a typed one, with nothing to tell the two apart, runs for no line that is only a subshell, and under `extdebug` its status decides whether a command runs. So bash 3.2 sends no C, and Ghostty's automatic integration skips bash 3.2. Its D comes from PS1 once `\#`, the command number, has moved, which neither a key binding nor an empty line does; readline sends a prompt's escapes again at each redraw, so that D goes again with it, the same status. Every mark of a command rides an expansion, which `promptvars` turns on, so while it is off at start the script says so rather than mark nothing without a word. fish 4.3 marks its own prompts and commands, fish 4.0 all but B, older ones none: the script adds what the running fish lacks, around the prompt shown, and wraps it again when the user's changes. PowerShell: the prompt reads `$?` first and sets it back for the user's prompt through `Write-Error '' -ErrorAction Ignore`, which adds nothing to `$Error`. A failure ends with `$LASTEXITCODE` when a program set it during the line, since each run stores a new object, equal or not, which `[object]::ReferenceEquals` tells apart, unless the line surely ends in PowerShell: its syntax tree, read as PSReadLine hands the line over, ends in a cmdlet, as `Get-Command` resolves the name, aliases through, in a name not found, or in no command; such a line ends with 1, a cmdlet's failure under `-ErrorAction Ignore` too. A line that ends in a block, a function, a script or a call through a variable takes the code of a program it ran: only a hook on every command lookup would name the last command run, and it slowed a loop of commands by half. `$LASTEXITCODE` is read through `Get-Variable`, since strict mode refuses an unset variable and a prompt that fails falls back to `PS>`. C goes out when PSReadLine hands over a line that is not blank; without PSReadLine the script says it marks no command. herdr passes no OSC 133 on, so a session running herdr keeps the status from before it.
- 2026-10-16: the view jumps between prompts, which the owner asked for, as Ghostty jumps: Cmd-Up and Cmd-Down, with Shift or without, on macOS, Ctrl-Shift-Up and Ctrl-Shift-Down elsewhere, scroll to the prompt that starts before or after the view's top; a prompt on the screen's last page means the bottom, which the view cannot pass, and with none there nothing moves. alacritty's rows hold no marks, and the tap, which reads before the parser, never sees the cursor, so the tap rewrites what goes to the parser: an OSC that may be a mark is held whole until it ends and read as `marks` reads it, and a prompt's start becomes an OSC 8 link of the terminal's own, its target `\x7f`, which no program names, its id `\x7f` and the prompt's number; the prompt's end closes it. The parser stamps each cell the prompt prints with the link, SGR 0 keeps it, and it rides the cells through scroll, reflow and history; a prompt starts on the first row that shows its number, blank rows within it passed. A program's own link keeps its target and takes the prompt's number in its id: one opened inside a prompt carries it, and a prompt that starts inside one opens it again with the number, a target of 256 bytes at most, and gives back its own id at the prompt's end; its end inside a prompt takes the prompt's link up again. A cursor restore that brings back a link passes the rewrite by. An OSC held, a body of 4096 bytes at most as the scanner keeps it and twice that with the controls vte skips, shows nothing until it ends; controls vte runs inside an escape go on before it, so only the ESC waits. A rewrite makes a sequence 36 times its size at most, a mark of 8 bytes a link of 276, so the tap reads a 36th of the buffer's room. Links, Cmd-press among them, pass over the prompt's link. The tap's OSC scanner ends a body as vte does, at BEL, ESC, CAN or SUB, and skips the controls vte skips, so a mark and its link agree.
- 2026-10-16: the view jumps between prompts, which the owner asked for, as Ghostty jumps: Cmd-Up and Cmd-Down, with Shift or without, on macOS, Ctrl-Shift-Up and Ctrl-Shift-Down elsewhere, scroll to the prompt that starts before or after the view's top; a prompt on the screen's last page means the bottom, which the view cannot pass, and with none there nothing moves. alacritty's rows hold no marks, and the tap, which reads before the parser, never sees the cursor, so the tap rewrites what goes to the parser: an OSC that may be a mark is held whole until it ends and read as `marks` reads it, and a prompt's start becomes an OSC 8 link of the terminal's own, its target `\x7f`, which no program names, its id `\x7f` and the prompt's number; the prompt's end closes it. The parser stamps each cell the prompt prints with the link, SGR 0 keeps it, and it rides the cells through scroll, reflow and history; a prompt starts on the first row that shows its number, blank rows within it passed. A program's own link keeps its target and takes the prompt's number in its id: one opened inside a prompt carries it, and a prompt that starts inside one opens it again with the number and gives back its own id at the prompt's end, its target kept whole as the scanner keeps it; its end inside a prompt takes the prompt's link up again. A cursor restore that brings back a link passes the rewrite by. An OSC held, a body of 4096 bytes at most as the scanner keeps it and twice that with the controls vte skips, shows nothing until it ends; controls vte runs inside an escape go on before an ESC that waits, and only an ESC from ground waits: one that ends an OSC goes on with it, so what follows runs where the parser stands. A rewrite turns a mark of 8 bytes inside a program's link into that link again, its target up to 4096 bytes, so the tap reads about 2 KB of the buffer's room at a time, which moved 3 MB through a pty as fast as larger reads. Links, Cmd-press among them, pass over the prompt's link. The tap's OSC scanner ends a body as vte does, at BEL, ESC, CAN or SUB, and skips the controls vte skips, so a mark and its link agree.
+46 -36
View File
@@ -16,11 +16,11 @@ const NUMBERED: char = '\u{7f}';
/// What ends a prompt's link.
const CLOSE: &[u8] = b"\x1b]8;;\x07";
/// The longest target of a program's link kept, to open it again with a prompt's number.
const TARGET: usize = 256;
/// What a held sequence's rewrite comes to at most: a link with the longest target the scanner keeps.
pub(super) const SLACK: usize = MOST + 32;
/// How many times its size a rewrite makes a sequence at most: a mark of 8 bytes becomes a link of 276.
pub(super) const GROWTH: usize = 36;
/// How many times its size a rewrite makes a sequence at most: a mark of 8 bytes inside a program's link becomes that link again.
pub(super) const GROWTH: usize = SLACK / 8 + 1;
const ESC: u8 = 0x1b;
@@ -33,8 +33,8 @@ fn ignored(byte: u8) -> bool {
enum State {
#[default]
Ground,
/// After an ESC, held until the next byte says whether an OSC starts.
Escape,
/// After an ESC, which waits while the parser stands where it stood before it, else has gone on.
Escape { sent: bool },
/// An OSC held whole, since it may be a mark or a link to rewrite.
Held,
/// An OSC passed on as it comes.
@@ -51,11 +51,11 @@ pub(crate) struct Prompts {
body: Vec<u8>,
/// The body passed the scanner's bound, so it counts for nothing.
overflow: bool,
/// The held OSC's ESC has gone on: it ended the OSC before.
sent: bool,
/// The prompt open now, from its start mark to its end.
prompt: Option<u32>,
/// A program's own link is open.
user: bool,
/// Its target, when short enough to keep.
/// The target of a program's link open now, to open it again with a prompt's number and without.
target: Option<Vec<u8>>,
/// The last prompt's number.
count: u32,
@@ -82,32 +82,33 @@ impl Prompts {
self.state = match (self.state, byte) {
(State::Ground, ESC) => {
self.held.push(ESC);
State::Escape
State::Escape { sent: false }
}
(State::Ground, _) => {
self.out.push(byte);
State::Ground
}
(State::Escape, b']') => {
(State::Escape { sent }, b']') => {
self.held.push(byte);
self.sent = sent;
self.body.clear();
self.overflow = false;
State::Held
}
(State::Escape, ESC) => {
(State::Escape { .. }, ESC) => {
self.release();
self.held.push(ESC);
State::Escape
State::Escape { sent: false }
}
// vte runs these and stays in the escape, so they go on before it, and only the ESC waits.
(State::Escape, _) if byte == 0x07 || ignored(byte) => {
// vte runs these and stays in the escape; one that waits lets them on first, where the parser stands the same.
(State::Escape { sent }, _) if byte == 0x07 || ignored(byte) => {
self.out.push(byte);
State::Escape
State::Escape { sent }
}
(State::Escape, _) => {
(State::Escape { .. }, _) => {
// A full reset clears the link the parser writes with.
if byte == b'c' {
(self.prompt, self.user, self.target) = (None, false, None);
(self.prompt, self.target) = (None, None);
}
self.held.push(byte);
self.release();
@@ -117,10 +118,15 @@ impl Prompts {
self.end(Some(byte));
State::Ground
}
// An ESC ends the OSC: after a rewrite the parser stands in ground, else inside the OSC, so the ESC goes on.
(State::Held | State::Passing, ESC) => {
self.end(None);
self.held.push(ESC);
State::Escape
if self.end(None) {
self.held.push(ESC);
State::Escape { sent: false }
} else {
self.out.push(ESC);
State::Escape { sent: true }
}
}
(State::Held, _) => {
self.held.push(byte);
@@ -168,8 +174,8 @@ impl Prompts {
self.out.append(&mut self.held);
}
/// The OSC ends at `end`, BEL, CAN or SUB, or at an ESC when none.
fn end(&mut self, end: Option<u8>) {
/// The OSC ends at `end`, BEL, CAN or SUB, or at an ESC when none; true when it was rewritten.
fn end(&mut self, end: Option<u8>) -> bool {
let rewrite = if self.overflow {
None
} else if let Some(opens) = self.body.strip_prefix(b"133;").and_then(mark) {
@@ -177,21 +183,19 @@ impl Prompts {
self.count = self.count.wrapping_add(1);
self.prompt = Some(self.count);
// A prompt inside a program's link carries its number on that link.
match (self.user, &self.target) {
(false, _) => Some(link(self.count, LINK.as_bytes())),
(true, Some(target)) => Some(link(self.count, target)),
(true, None) => None,
}
Some(match &self.target {
Some(target) => link(self.count, target),
None => link(self.count, LINK.as_bytes()),
})
} else {
match (self.prompt.take(), self.user, &self.target) {
(Some(_), false, _) => Some(CLOSE.to_vec()),
(Some(_), true, Some(target)) => Some(plain(target)),
_ => None,
match (self.prompt.take(), &self.target) {
(Some(_), Some(target)) => Some(plain(target)),
(Some(_), None) => Some(CLOSE.to_vec()),
(None, _) => None,
}
}
} else if let Some((opens, target)) = user(&self.body) {
self.user = opens;
self.target = (opens && target.len() <= TARGET).then(|| target.to_vec());
self.target = opens.then(|| target.to_vec());
// Inside a prompt a program's link carries the prompt's number; its end takes the prompt's link up again.
let target = if opens { target } else { LINK.as_bytes() };
self.prompt.map(|number| link(number, target))
@@ -201,15 +205,21 @@ impl Prompts {
match (rewrite, self.state) {
(Some(bytes), State::Held) => {
self.held.clear();
self.out.extend_from_slice(&bytes);
// An ESC that went on already starts the link.
self.out.extend_from_slice(&bytes[usize::from(self.sent)..]);
// CAN and SUB end the OSC and run as well.
self.out.extend(end.filter(|&byte| byte != 0x07));
true
}
(_, State::Held) => {
self.held.extend(end);
self.release();
false
}
_ => {
self.out.extend(end);
false
}
_ => self.out.extend(end),
}
}
}
+52 -10
View File
@@ -31,7 +31,7 @@ fn parsed(bytes: &[u8], columns: usize, lines: usize) -> Term<VoidListener> {
}
/// The target of the link at a cell.
fn target(term: &Term<VoidListener>, line: i32, column: usize) -> Option<String> {
fn target_of(term: &Term<VoidListener>, line: i32, column: usize) -> Option<String> {
let cell = &term.grid()[Line(line)][Column(column)];
cell.hyperlink().map(|link| link.uri().to_string())
}
@@ -136,7 +136,7 @@ fn a_program_s_link_inside_a_prompt_carries_its_number() {
Some(1),
"a prompt all in a link"
);
assert_eq!(target(&whole, 0, 6).as_deref(), Some("file:///work"));
assert_eq!(target_of(&whole, 0, 6).as_deref(), Some("file:///work"));
let first = b"\x1b]133;A\x07\x1b]8;;file:///w\x1b\\/work\r\n\x1b]8;;\x1b\\$ \x1b]133;B\x07\r\no\r\no\r\no";
let term = parsed(first, 8, 3);
assert_eq!(jump(&term, true), Some(2), "its linked first row starts it");
@@ -159,7 +159,7 @@ fn a_program_s_link_inside_a_prompt_carries_its_number() {
.is_some_and(|link| !link.id().starts_with(NUMBERED))
);
assert_eq!(
target(&open, 0, 2).as_deref(),
target_of(&open, 0, 2).as_deref(),
Some("file:///x"),
"its own again past the prompt"
);
@@ -169,6 +169,48 @@ fn a_program_s_link_inside_a_prompt_carries_its_number() {
);
}
#[test]
fn a_long_target_numbers_a_prompt_in_either_order() {
let target = format!("https://e.test/{}", "a".repeat(249));
let open = format!("\x1b]8;;{target}\x1b\\");
let before = format!("{open}x\x1b]133;A\x07y\x1b]133;B\x07z\x1b]8;;\x1b\\");
let inside = format!("x\x1b]133;A\x07{open}y\x1b]133;B\x07z\x1b]8;;\x1b\\");
for bytes in [before, inside] {
let term = parsed(bytes.as_bytes(), 8, 2);
let row = &term.grid()[Line(0)];
let numbered = |column: usize| {
row[Column(column)]
.hyperlink()
.is_some_and(|link| link.id().starts_with(NUMBERED))
};
assert_eq!(prompt(row), Some(1), "{bytes:?}");
assert!(
numbered(1) && !numbered(2),
"the prompt's cell alone: {bytes:?}"
);
assert_eq!(target_of(&term, 0, 2).as_deref(), Some(target.as_str()));
}
}
#[test]
fn controls_after_an_osc_s_closing_escape_run_where_vte_runs_them() {
let whole: &[u8] = b"LEFT\x1b]0;title\x1b\r\n\\RIGHT";
for cut in 0..=whole.len() {
let (first, second) = whole.split_at(cut);
assert_eq!(passed(&[first, second]), whole, "cut at {cut}");
}
let term = parsed(whole, 8, 2);
let line = |at: i32| -> String {
term.grid()[Line(at)]
.into_iter()
.map(|cell| cell.c)
.collect()
};
assert_eq!((line(0).trim_end(), line(1).trim_end()), ("LEFT", "RIGHT"));
let marked = parsed(b"\x1b]133;A\x1b\r\n\\$", 8, 2);
assert_eq!(prompt(&marked.grid()[Line(1)]), Some(1));
}
#[test]
fn controls_vte_runs_inside_an_escape_go_on_before_the_link() {
assert_eq!(
@@ -186,8 +228,8 @@ fn a_prompt_s_cells_carry_its_link_through_sgr_0_and_its_input_none() {
);
assert_eq!(prompt(&term.grid()[Line(0)]), None);
assert_eq!(prompt(&term.grid()[Line(1)]), Some(1));
assert_eq!(target(&term, 1, 1).as_deref(), Some(LINK));
assert_eq!(target(&term, 1, 2), None, "input");
assert_eq!(target_of(&term, 1, 1).as_deref(), Some(LINK));
assert_eq!(target_of(&term, 1, 2), None, "input");
}
#[test]
@@ -197,21 +239,21 @@ fn a_program_s_own_link_and_the_prompt_s_keep_each_other() {
12,
2,
);
assert_eq!(target(&term, 0, 0).as_deref(), Some("file:///work"));
assert_eq!(target(&term, 0, 6).as_deref(), Some(LINK));
assert_eq!(target_of(&term, 0, 0).as_deref(), Some("file:///work"));
assert_eq!(target_of(&term, 0, 6).as_deref(), Some(LINK));
assert_eq!(prompt(&term.grid()[Line(0)]), Some(1));
let term = parsed(
b"\x1b]8;;file:///x\x1b\\x\x1b]133;A\x07y\x1b]8;;\x1b\\z\x1b]133;B\x07",
12,
2,
);
assert_eq!(target(&term, 0, 0).as_deref(), Some("file:///x"));
assert_eq!(target_of(&term, 0, 0).as_deref(), Some("file:///x"));
assert_eq!(
target(&term, 0, 1).as_deref(),
target_of(&term, 0, 1).as_deref(),
Some("file:///x"),
"a prompt inside a link keeps it"
);
assert_eq!(target(&term, 0, 2).as_deref(), Some(LINK));
assert_eq!(target_of(&term, 0, 2).as_deref(), Some(LINK));
}
/// Prompts on lines 0, 3 and 6 of a history four lines high, the last on the screen.
+4 -1
View File
@@ -45,7 +45,10 @@ impl<P> Tapped<P> {
impl<P: EventedReadWrite> io::Read for Tapped<P> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
// What a rewrite adds stays inside the buffer, with room for the held sequence's own.
let take = buf.len().saturating_sub(self.prompts.held() + 300) / prompts::GROWTH;
let room = buf
.len()
.saturating_sub(self.prompts.held() + prompts::SLACK);
let take = room / prompts::GROWTH;
if take == 0 {
return Ok(0);
}
+1 -1
View File
@@ -52,7 +52,7 @@ What no check reached: no real KWin drew the blur; of Wayland desktops, only Hyp
| PTY through alacritty's event loop; OSC 7 and OSC 133 scanned on the reader | T04, T34 | `term::pty`, `term::tap` tests; round 3 failed at the cap, its fix stands on a red-green test | Pass |
| Shell integration for zsh, bash, fish, PowerShell; a shell without it reports nothing and the panels say so | T05 | `term::shell` tests on macOS and Linux | Pass |
| Prompt marks, OSC 133, from the shell integration and any program; each session keeps its last command's status | T34 | `term::marks` tests: each letter and option as Ghostty reads them; `term::tap` and `term::pty` tests: marks split across reads reach the session in order; `term::shell` tests: zsh, bash, fish and PowerShell send C, D;1, A and B around a failing command, and the user's prompt still shows the failure, a zsh theme's that sets PS1 in its own hook too, whose first prompt is marked before any command, and `$pipestatus` stays; bash's lone subshell ends with D;7, `$_` stays, a command runs under `extdebug` after a failure, a key binding marks nothing, the user's DEBUG trap runs, bash 3.2 sends no C, and bash says at start why it marks nothing while `promptvars` is off; PowerShell reads 1 for a cmdlet's failure under `-ErrorAction Ignore` after a success, a native code twice, a native failure's code under `$PSNativeCommandUseErrorActionPreference`, on a line that mixes a program and a cmdlet the cmdlet's 1, and through a call by variable the program's code; on macOS fish 4.9 marks itself and bash 3.2 marks no C, in the Linux container fish 4.0 gets B and bash 5.2 marks C through PS0, CI's Ubuntu runs fish 3.7, which the script marks whole; by hand in a pty, PowerShell under strict mode, and a cmdlet's failure after a native one's reads 1 | Pass |
| Jump to the prompt before or after the view's top, as Ghostty jumps | T35 | `term::prompts` tests: a mark becomes its prompt's numbered link, read as the session reads marks, whole when split across reads at every byte, and other sequences, `133;AB` among them, pass as they came; a prompt's cells carry its link through SGR 0 and a resize, its input none; a program's own link and the prompt's keep each other, a program's link inside a prompt carries the prompt's number, a prompt inside a link left open too; a long escape of controls keeps the pty reading; a prompt over rows starts at its first, blank rows within it too; up and down land as Ghostty's `scrollPrompt`; `term::links`: a prompt's link is no target; `term::pty`: through the event loop the prompt's cell alone carries it; in the app on macOS, Cmd-Up once and twice put PROMPT-3 and PROMPT-2 at the top, and Cmd-Shift-Down and Cmd-Down came back to the screen's bottom; the review's fixtures of a prompt that starts with a link and of a prompt with a blank row jumped to PROMPT-3-FIRST, and its fixture of `133;AB` marks found no prompt; the second review's fixtures of a prompt all in a link and of a linked first row jumped to PROMPT-3-FIRST. The keys off macOS are bound, not run | Pass |
| Jump to the prompt before or after the view's top, as Ghostty jumps | T35 | `term::prompts` tests: a mark becomes its prompt's numbered link, read as the session reads marks, whole when split across reads at every byte, and other sequences, `133;AB` among them, pass as they came; a prompt's cells carry its link through SGR 0 and a resize, its input none; a program's own link and the prompt's keep each other, a program's link inside a prompt carries the prompt's number, a prompt inside a link left open too, a target of 264 bytes in either order; controls after an OSC's closing ESC keep their line; a long escape of controls keeps the pty reading; a prompt over rows starts at its first, blank rows within it too; up and down land as Ghostty's `scrollPrompt`; `term::links`: a prompt's link is no target; `term::pty`: through the event loop the prompt's cell alone carries it; in the app on macOS, Cmd-Up once and twice put PROMPT-3 and PROMPT-2 at the top, and Cmd-Shift-Down and Cmd-Down came back to the screen's bottom; the review's fixtures of a prompt that starts with a link and of a prompt with a blank row jumped to PROMPT-3-FIRST, and its fixture of `133;AB` marks found no prompt; the second review's fixtures of a prompt all in a link and of a linked first row jumped to PROMPT-3-FIRST. The keys off macOS are bound, not run | Pass |
| 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 |
| 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 |