T34: review round 2: bash 3.2 drops its DEBUG trap, which ran for key bindings, missed lone subshells and decided commands under extdebug, so it sends no C, and its D comes from the prompt by the command number; PowerShell takes a program's code only when the line's last pipeline ends in it, as its syntax tree and Get-Command name it; each check waits for the prompt before its line, so a repeated status is the line's own
CI / platforms (ubuntu-24.04) (push) Successful in 1m35s
CI / platforms (windows-latest) (push) Successful in 1m41s
CI / macos (push) Successful in 11m11s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-16 23:13:42 -04:00
1 parent 0a22200725
commit d4bc6e69fa
6 files changed
+89 -45

No files matched your search

+1 -1
View File
@@ -96,4 +96,4 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- 2026-10-16: herdr 0.9 keeps its panes' OSC 7 and writes only a title, notifications, OSC 52 and bells to the terminal it runs in, so no terminal hears a folder from inside it; herdr's server knows each pane's `cwd`, the folder its shell reported, else the shell's own. On macOS and Linux each session asks twice a second which process group leads its pty's foreground (`tcgetpgrp` on a copy of the master) and what executable that group's leader runs: an exec keeps the group and swaps the executable, so `exec herdr` from a shell is a new program, and one that did not read is tried again; on Linux the ` (deleted)` mark an update leaves on a running executable is set aside. When that executable is `herdr` and its arguments start herdr's terminal client (no command, `session attach NAME` or `client`), ELYTerminal reads the client's arguments and variables (`/proc` on Linux; `KERN_PROCARGS2` on macOS, which hides only the variables of Apple's own programs) and finds its socket as herdr 0.9 does: `--session NAME`, then `HERDR_SOCKET_PATH`, then `HERDR_SESSION`, under `$XDG_CONFIG_HOME/herdr`, else `~/.config/herdr`; a relative socket lies in the client's folder, as herdr reads it (`/proc/PID/cwd`; `proc_pidinfo` on macOS). It asks that socket for `session.snapshot` twice a second, a connection each, since herdr closes one after its reply, and reports the focused pane's `cwd` as the session's folder when it changes and again each time herdr takes the foreground; an answer that lands after the shell reported a folder is dropped, since the shell reports only once herdr has left. Focus belongs to herdr's server, so clients of one server follow one focus; a client of another machine's server (`--remote`, `--machine`) is logged, not followed. A failure is logged once while it lasts. On Windows, which has no foreground group, a Toolhelp32 snapshot lists the `herdr.exe` processes at or under the session's shell, none under another herdr, since what a herdr starts, its server too, is its own; when that list changes, `sysinfo` reads their arguments and variables, nearest first, names in upper case, as Windows matches them, and the first terminal client is followed, a server or a command beside it passed by; its folder is found as herdr 0.9 finds it there, `XDG_CONFIG_HOME`, then `APPDATA`, then `USERPROFILE\AppData\Roaming`, then `HOME\.config`; and its server answers on the pipe herdr names `\\.\pipe\` and the socket's path as written, relative or not. A busy pipe is waited for, as herdr's client waits, and the pipe's server may identify ELYTerminal but never act as it. The pipe has no timeout: a herdr that never answers holds that session's follow until it answers or exits.
- 2026-10-16: the main window opens at the size it was left at, which the owner asked for on macOS, Windows and Linux: `window_width` and `window_height` in the settings hold the size gpui's `window_bounds` gives 0.5 s after the bounds settle, saved through `SettingsStore::update`, which writes only a change, so a drag saves once and a move saves nothing. A maximized, full-screen or hidden window keeps the size from before; its state and size are read once they settle, since X11 marks a window maximized after its new size, gpui's X11 window then gives a maximized window's own size, and it calls a maximized window unmaximized while minimized. A window that shows again, uncovered, restored or back on its screen, is read again, so a size it took just before it hid is kept; a size it takes in the half second before it closes, or before it hides until the app quits, is lost. The window is no smaller than 100 by 100, the least the settings keep, or a smaller size the platform allows could not be kept. The window opens centered at whole points, since macOS grows a window by a point on a side whose edge falls on a half point; its place is not kept, and a kept side past the primary display's visible area shrinks to it, logged. A size outside 100 to 16384 stops the start, as any value out of range does.
- 2026-10-16: the sessions and right columns resize by a drag on their border toward the terminal, which the owner asked for on macOS, Windows and Linux. Ely's `ShellColumn::resizable`, added to Ely for this, puts Ely's `resize_handle` on the border toward the filling column, the sessions column's right and the right column's left: the handle draws that border, a filled hairline, since gpui paints a border no wider than half its box, and the line it drew at first showed half a pixel on macOS and none on X11; it occludes the header under it, so a press there drags the border, not the window, on Windows too, where an unoccluded drag region takes the press as the caption. A drag sets off from the wider of the width drawn and the one kept: a header's parts may hold a column wider, and a window too narrow for both widths squeezes them, where a drag from the squeezed width jumped back against the pointer; a drag that has not moved sideways changes nothing, and one that comes back where it set off gives the width it had. A set width in Ely became a flex basis, so a column holds its header's own parts: at a set width under them, Windows' caption buttons ran out of the window. The widths, 160 to 960 pixels, show at once and are saved in `sessions_width` and `panel_width` through `SettingsStore::update` 0.5 s after a drag rests, as the window's size is; a drag in the half second before the window closes is lost, and a save the store refuses puts the columns back. A window too narrow for both yields the terminal its header's width, as Ely's set widths give way.
- 2026-10-16: prompts and commands carry OSC 133 marks, which the owner asked for, read as Ghostty reads them: A or N starts a prompt, B or I ends it, C starts a command's output, and D ends the command with its status, D's first option, none when that is no number. The tap reads any program's marks before the parser, so it never sees the cursor: A's fresh line and L, which need it, are not done, and P adds nothing to A. Each session keeps the status its last command ended with. The integration marks what the shell leaves unmarked, and the user's prompt still sees the command's status. zsh: precmd reads `$?` and sends D after a command ran, preexec sends C, and a last precmd hook wraps PS1 in A and B under `prompt_percent`; 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 sends D once a command ran, 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; both need `promptvars`, on by default. macOS's bash 3.2 has no PS0: a DEBUG trap armed at the prompt sends C and notes it, then runs any trap the user set, with `$?` kept, and `$_` too, since the call is passed `$_` and bash sets `$_` from a call's last word. bash 3.2 runs no trap for a line that is only a subshell, so such a line sends no C, and its D comes from PS1 once `\#`, the command number, has moved; readline sends a prompt's escapes again at each redraw, so D rides PS1 only then. 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 native program set it during the command, since each run stores a new object, equal or not, which `[object]::ReferenceEquals` tells apart, and with 1 otherwise, a cmdlet's under `-ErrorAction Ignore` too; it 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. herdr passes no OSC 133 on, so a session running herdr keeps the status from before it.
- 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`; 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. 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 the line's last pipeline ends in a program or a script, as the line's syntax tree names it and `Get-Command` resolves it, aliases through, as the line is handed over, and that program set `$LASTEXITCODE` during the line, since each run stores a new object, equal or not, which `[object]::ReferenceEquals` tells apart; any other failure ends with 1, a cmdlet's under `-ErrorAction Ignore` too. A hook on every command lookup would name the last command run, but 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. herdr passes no OSC 133 on, so a session running herdr keeps the status from before it.
+1 -1
View File
@@ -44,7 +44,7 @@ Ghostty is the bar for what the terminal does. ELYTerminal owns its engine on `a
- PTY: alacritty's `tty` and event loop. The pty is wrapped so its reader passes bytes through an OSC scanner. The scanner reports the working directory and prompt marks.
- Shell integration for zsh, bash, fish and PowerShell, injected at launch as Ghostty does: each prompt sends OSC 7 and a title. A shell without integration reports nothing, and the panels say so.
- Prompt marks, OSC 133: the shell integration marks where each prompt starts (A) and ends (B), where a command starts (C), and where it ends with its exit status (D); the terminal reads any program's OSC 133. Cmd-Up and Cmd-Down, and Cmd-Shift-Up and Cmd-Shift-Down, on macOS, Ctrl-Shift-Up and Ctrl-Shift-Down elsewhere, scroll to the prompt before or after the view's top, as in Ghostty.
- Prompt marks, OSC 133: the shell integration marks where each prompt starts (A) and ends (B), where a command starts (C), and where it ends with its exit status (D), all but C on macOS's bash 3.2, which has no hook for it; the terminal reads any program's OSC 133. Cmd-Up and Cmd-Down, and Cmd-Shift-Up and Cmd-Shift-Down, on macOS, Ctrl-Shift-Up and Ctrl-Shift-Down elsewhere, scroll to the prompt before or after the view's top, as in Ghostty.
- Program status, OSC 7501 revision 0.4: each terminal keeps the records programs report (state, id, kind, progress, app, title, msg), checked as the spec checks them; a new prompt drops working, blocked and idle records, a full reset drops them all; the terminal answers the support query with the states and kinds it takes.
- Progress, OSC 9;4: set, error, indeterminate and pause stand as the root record until a program reports OSC 7501, until a full reset.
- Notifications: OSC 9 (iTerm2), OSC 777 notify (rxvt) and OSC 99 (kitty: title and body in chunks, base64, ids, occasions, focus and report on activation, close, and its query); ConEmu's other OSC 9 commands are no notifications.
+4 -23
View File
@@ -13,37 +13,19 @@ else
[ -r ~/.bashrc ] && builtin source ~/.bashrc
fi
_ely_ran=
_ely_sent=
_ely_n=0
_ely_armed=
_ely_ps0=
if (( BASH_VERSINFO[0] * 100 + BASH_VERSINFO[1] >= 404 )); then
# PS0 runs in the shell, so its subscript can note that a command ran.
_ely_ps0='${_ely_note[_ely_ran=1]-}\e]133;C\a'
else
# bash 3.2 has no PS0: the DEBUG trap sends C, then runs the user's own trap with $? and $_ kept.
_ely_debug() {
builtin local status=$?
if [ -n "$_ely_armed" ] && [[ $BASH_COMMAND != _ely_* ]]; then
_ely_armed=
_ely_ran=1
builtin printf '\e]133;C\a'
fi
builtin return $status
}
builtin eval "_ely_trap=($(builtin trap -p DEBUG))"
builtin trap "_ely_debug \"\$_\"${_ely_trap[2]:+; ${_ely_trap[2]}}" DEBUG
builtin unset _ely_trap
fi
_ely_prompt() {
builtin local ret=$? LC_ALL=C char url= i title
_ely_armed=
_ely_sent=$_ely_ran
if [ -n "$_ely_ran" ]; then
builtin printf '\e]133;D;%s\a' "$ret"
_ely_ran=
fi
_ely_d=([1]=$'\e]133;D;'"$ret"$'\a')
[ -n "$_ely_ps0" ] || _ely_d=([1]=$'\e]133;D;'"$ret"$'\a')
for (( i = 0; i < ${#PWD}; i++ )); do
char=${PWD:i:1}
case $char in
@@ -58,9 +40,9 @@ _ely_prompt() {
# Last, after anything that sets PS1 or PS0.
_ely_mark() {
if [[ $PS1 != *'\e]133;A'* ]]; then
if builtin shopt -q promptvars; then
# bash 3.2 traps no line that is only a subshell; \# counts it, so the prompt sends its D.
PS1='\[${_ely_d[!_ely_sent && _ely_n && \# != _ely_n]-}${_ely_note[_ely_n = \#]-}\e]133;A\a\]'"$PS1"'\[\e]133;B\a\]'
if [ -z "$_ely_ps0" ] && builtin shopt -q promptvars; then
# bash 3.2 has no PS0; \#, the command number, moves once a command ran.
PS1='\[${_ely_d[_ely_n && \# != _ely_n]-}${_ely_note[_ely_n = \#]-}\e]133;A\a\]'"$PS1"'\[\e]133;B\a\]'
else
PS1='\[\e]133;A\a\]'"$PS1"'\[\e]133;B\a\]'
fi
@@ -68,6 +50,5 @@ _ely_mark() {
if [ -n "$_ely_ps0" ] && builtin shopt -q promptvars && [[ ${PS0-} != *"$_ely_ps0"* ]]; then
PS0=$_ely_ps0${PS0-}
fi
_ely_armed=1
}
PROMPT_COMMAND="_ely_prompt${PROMPT_COMMAND:+;$PROMPT_COMMAND};_ely_mark"
+15 -1
View File
@@ -11,7 +11,7 @@ function global:prompt {
# Each native run sets a new $LASTEXITCODE, equal or not; strict mode refuses an unset one.
$exit = Get-Variable LASTEXITCODE -Scope Global -ValueOnly -ErrorAction Ignore
$ran = $null -ne $exit -and -not [object]::ReferenceEquals($exit, $global:ElyExit)
$code = if ($ran -and $exit -ne 0) { $exit } else { 1 }
$code = if ($global:ElyNative -and $ran -and $exit -ne 0) { $exit } else { 1 }
}
[Console]::Write("$esc]133;D;$code$bel")
$global:ElyRan = $false
@@ -38,6 +38,20 @@ if (Test-Path Function:\PSConsoleHostReadLine) {
if (-not [string]::IsNullOrWhiteSpace($line)) {
$global:ElyRan = $true
$global:ElyExit = Get-Variable LASTEXITCODE -Scope Global -ValueOnly -ErrorAction Ignore
# A program's exit code is the line's when the line's last pipeline ends in it.
$global:ElyNative = $false
$ast = [System.Management.Automation.Language.Parser]::ParseInput($line, [ref]$null, [ref]$null)
# Strict mode refuses .Count on one statement alone, so the statements stay a list.
$statements = @(if ($ast.EndBlock) { $ast.EndBlock.Statements })
$last = if ($statements.Count) { $statements[$statements.Count - 1] }
if ($last -is [System.Management.Automation.Language.PipelineAst]) {
$elements = $last.PipelineElements
$end = $elements[$elements.Count - 1]
$name = if ($end -is [System.Management.Automation.Language.CommandAst]) { $end.GetCommandName() }
$info = if ($name) { Get-Command -Name $name -ErrorAction Ignore | Select-Object -First 1 }
while ($info -and $info.CommandType -eq 'Alias') { $info = $info.ResolvedCommand }
$global:ElyNative = $null -ne $info -and $info.CommandType -in 'Application', 'ExternalScript'
}
[Console]::Write("$([char]27)]133;C$([char]7)")
}
$line
+67 -18
View File
@@ -47,6 +47,8 @@ struct Run {
folders: Vec<PathBuf>,
titles: Vec<String>,
marks: Vec<Mark>,
/// Lines typed, each followed by a prompt of its own.
lines: usize,
deadline: Instant,
}
@@ -87,6 +89,7 @@ impl Run {
folders: Vec::new(),
titles: Vec::new(),
marks: Vec::new(),
lines: 0,
deadline: Instant::now() + Duration::from_secs(30),
}
}
@@ -122,6 +125,7 @@ impl Run {
fn send(&mut self, line: &str) {
self.until("line editor", |run| run.pty.reads_keys());
self.pty.write(format!("{line}\r").into_bytes());
self.lines += 1;
}
fn shows(&self, text: &str) -> bool {
@@ -156,11 +160,20 @@ impl Run {
self.until("inner folder", |run| run.folders.contains(&inner));
}
/// Types `line`, the last command sent, and waits for the marks it ends with.
/// Types `line` once the prompt before it is marked, then waits for the marks it ends with.
fn ends(&mut self, line: &str, marks: &[Mark]) {
let prompts = self.lines + 1;
self.until("the prompt's marks", |run| {
run.marks
.iter()
.filter(|mark| **mark == Mark::Input)
.count()
>= prompts
});
let from = self.marks.len();
self.send(line);
self.until(&format!("the marks of {line}"), |run| {
run.marks.ends_with(marks)
run.marks[from..].ends_with(marks)
});
}
@@ -185,6 +198,25 @@ impl Run {
}
}
/// Whether this bash has PS0, 4.4 on, so it marks a command's start.
fn bash_ps0(shell: &str) -> bool {
let version = std::process::Command::new(shell)
.args(["-c", "echo $((BASH_VERSINFO[0] * 100 + BASH_VERSINFO[1]))"])
.output()
.expect("bash runs");
let version: u32 = String::from_utf8(version.stdout)
.expect("utf-8")
.trim()
.parse()
.expect("a version");
version >= 404
}
/// The marks a bash command that ends with `status` leaves; bash 3.2 marks no start.
fn bash_ended(ps0: bool, status: i32) -> Vec<Mark> {
ended(status)[usize::from(!ps0)..].to_vec()
}
/// The marks a command that ends with `status` leaves: its output, its end, the next prompt.
fn ended(status: i32) -> [Mark; 4] {
[
@@ -250,35 +282,46 @@ fn zsh_reports_its_folder_byte_for_byte_and_its_titles() {
#[test]
fn bash_reports_its_folder_byte_for_byte_and_keeps_the_status() {
let status = r#"PROMPT_COMMAND='printf "USER_STATUS=%s\n" "$?"'"#;
let status = r#"PROMPT_COMMAND='printf "USER_STATUS=%s\n" "$?"'; shopt -s extdebug"#;
let files = [(".bashrc", status), (".bash_profile", status)];
let mut run = Run::start(&installed("bash"), &files);
let shell = installed("bash");
let ps0 = bash_ps0(&shell);
let mut run = Run::start(&shell, &files);
run.walk("declare -F _ely_prompt >/dev/null && echo ELY-LOAD''ED");
run.until("title", |run| {
run.titles.iter().any(|title| title == "in ner%xy")
});
run.ends("false", &ended(1));
run.ends("false", &bash_ended(ps0, 1));
run.until("kept status", |run| run.shows("USER_STATUS=1"));
assert!(run.shows("$ false"), "{}", run.screen());
// bash 3.2 traps no part of a lone subshell, so its C may not come.
run.ends(
"(exit 7)",
&[Mark::Done(Some(7)), Mark::Prompt, Mark::Input],
);
// Under extdebug a DEBUG trap that fails skips its command.
run.ends("touch ran", &bash_ended(ps0, 0));
assert!(run.start.join(INNER).join("ran").exists());
run.ends("(exit 7)", &bash_ended(ps0, 7));
run.send(r#"ls / >/dev/null; echo "LAST=$_""#);
run.until("the last argument", |run| run.shows("LAST=/\n"));
run.finish();
}
#[test]
fn bash_runs_the_user_s_debug_trap_beside_its_own() {
let trap = r#"_user_trap() { USER_TRAP=yes; }; trap '_user_trap "$_"' DEBUG"#;
let files = [(".bashrc", trap), (".bash_profile", trap)];
let mut run = Run::start(&installed("bash"), &files);
fn bash_leaves_the_user_s_trap_and_keys_alone() {
let rc = r#"_user_trap() { USER_TRAP=yes; }; trap '_user_trap "$_"' DEBUG
bind -x '"\C-t": :'"#;
let files = [(".bashrc", rc), (".bash_profile", rc)];
let shell = installed("bash");
let ps0 = bash_ps0(&shell);
let mut run = Run::start(&shell, &files);
run.walk("declare -F _ely_prompt >/dev/null && echo ELY-LOAD''ED");
run.ends("false", &ended(1));
run.send(r#"echo "TRAP=$USER_TRAP""#);
run.until("the user's trap", |run| run.shows("TRAP=yes"));
run.ends("false", &bash_ended(ps0, 1));
// A key binding runs no command, so it marks none.
let from = run.marks.len();
run.pty.write(b"\x14".to_vec());
run.ends(r#"echo "TRAP=$USER_TRAP""#, &bash_ended(ps0, 0));
assert!(run.shows("TRAP=yes"), "{}", run.screen());
let starts = run.marks[from..]
.iter()
.filter(|mark| **mark == Mark::Output);
assert_eq!(starts.count(), usize::from(ps0), "{:?}", &run.marks[from..]);
run.finish();
}
@@ -313,12 +356,18 @@ fn powershell_reports_its_folder_byte_for_byte_and_keeps_the_status() {
run.ends("/usr/bin/false", &ended(1));
run.until("kept status", |run| run.shows("USER_SUCCESS=False"));
assert!(run.shows("> /usr/bin/false"), "{}", run.screen());
// A cmdlet's failure leaves no error behind, and a native code may repeat.
// A cmdlet's failure leaves no error behind, a native code may repeat, and the line's last pipeline counts.
run.ends("/usr/bin/true", &ended(0));
run.ends("Get-Item ./missing -ErrorAction Ignore", &ended(1));
run.ends("sh -c 'exit 3'", &ended(3));
run.ends("sh -c 'exit 3'", &ended(3));
run.ends("Get-Item ./missing", &ended(1));
run.ends("sh -c 'exit 7'; Get-Item ./missing", &ended(1));
run.ends(
"sh -c 'exit 7'; Get-Item ./missing -ErrorAction Ignore",
&ended(1),
);
run.ends("Get-Item ./missing; sh -c 'exit 7'", &ended(7));
let native = "$PSNativeCommandUseErrorActionPreference = $true; sh -c 'exit 7'";
run.ends(native, &ended(7));
run.finish();
+1 -1
View File
@@ -51,7 +51,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, and the user's DEBUG trap runs beside the script's; PowerShell reads 1 for a cmdlet's failure under `-ErrorAction Ignore` after a success, a native code twice, and a native failure's code under `$PSNativeCommandUseErrorActionPreference`; on macOS fish 4.9 marks itself and bash 3.2 marks C through its DEBUG trap, 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 |
| 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, and bash 3.2 sends no C; 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`, and on a line that mixes a program and a cmdlet the code of its last pipeline; on macOS fish 4.9 marks itself and bash 3.2 marks C through its DEBUG trap, 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 |
| 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 |