T59: OSC 133 B stamps the line typed after a prompt with a link of its own, numbered by the commands ended, so a shell whose prompt prints nothing, as fish's empty prompt, takes clicks from its first typed cell, and fish's right prompt drawn again at each key keeps the line
This commit is contained in:
6 files changed
+247
-80
No files matched your search
@@ -100,7 +100,7 @@ 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 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.
|
||||
- 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, B, opens a link of the same target over the line typed after it, its id `\x7f`, the commands ended so far, C or D each one, and `i`, so every prompt a shell draws again on one line, as fish draws its right prompt at each key, shares its input's number; C or D closes it. macOS's bash 3.2, which sends no C, stamps a command's output as input until its D. 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.
|
||||
- 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.
|
||||
@@ -114,4 +114,4 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
|
||||
- 2026-10-19: a drag moves Ely along the edge she stands on, which the owner asked for after 0.1.5. On macOS, Windows and X11 her glue moves her window after the pointer's place on the screen, `NSEvent`'s `mouseLocation`, `GetCursorPos` and `query_pointer`, since her window moves under the pointer, so a place in it would lag; her window keeps the pointer while the button is held, inside it or not, as AppKit, gpui's `SetCapture` and X's implicit grab give it. A Wayland popup cannot move, so there she moves once let go: the settings keep the place, and the twice-a-second check opens her popup there. She moves by whole points inside the pane, and `ely_left` keeps where she stands, 140 by default, where she stood before; a narrower pane holds her at its right end, and the log names where she is let go. Under Xorg's dummy screen xdotool dragged her 200 pixels and her window followed them; under Sway nested in Xorg, whose X input gives its seat a pointer, xdotool's drag reopened her popup 200 points on. Sway's own `seat cursor` commands send no motion while their press is held, and Sway draws no popup opened before its window settled, so a probe there reopens her before it drags.
|
||||
- 2026-10-19: Settings › Terminal checks for updates, which the owner asked for after 0.1.5: under Ely, a switch, on by default, checks at start and once a day, and Check now checks at once. The check runs `curl`, as git runs through its command line, so no HTTP or TLS crate joins the build; macOS and Windows 10 ship it, and a missing curl shows as a failed check. It asks Gitea's API for the latest published release, compares its tag's three numbers with the running version, and the row says what it found; a newer one brings Download, which opens the release's page in the browser, since replacing a running, signed app is a job of its own. An automatic check that finds one posts a system notification, at most once a day, whose press opens the page. Checks run off the main thread, one at a time, each start and finding logged; a failure shows its error in the row. Under Xorg's dummy screen the start's check found 0.1.5 the latest, Check now and the switch checked again, and a build named 0.1.4 showed 0.1.5 out with Download, which opened its page.
|
||||
- 2026-10-19: the terminal shows a scrollbar, which a user asked for in issue #2 after 0.1.5: a program that tracks the mouse takes the wheel, so the wheel alone could not reach the history. A thumb rides the view's right edge, over the padding, in Ely's scrollbar sizes and colors, its thickness, its least length and the foreground at 22 and 42 percent; its length is the screen's share of the history, and its place where the view stands. A drag moves the view through the history, whatever mouse mode the program asked for; the bar covers the grid's hitbox, so the terminal sees none of its presses, and a release with no press of its own reports nothing. Ely's `Scrollbar` reads a gpui `ScrollHandle`, and the terminal's place is alacritty's display offset, so the bar is the terminal's own, drawn as Ely draws hers and dragged through gpui's drag as hers is; gpui sets a drag's anchor at its first move past the threshold, so a pointer that jumps far at once loses that jump. The alternate screen holds no history, so it shows none. Under Xorg's dummy screen, 500 lines in, a drag to the top showed the first prompt and lines 1 to 39, and with mouse tracking on, which sent the wheel to the program, the bar still scrolled.
|
||||
- 2026-10-19: a plain click on the line the shell reads moves its cursor there, which a user asked for after 0.1.6, as Ghostty's `cursor-click-to-move`: a press with no modifier, one click, let go in its own cell with no selection, while OSC 133 says the shell reads a line, B since the last A or C, and the program tracks no mouse at the release. A shell whose prompt start says `click_events=1` or `2`, as fish from 4.1 says on its own, hears a click past the prompt's cells, none on them or on a prompt cell after the cursor, itself, a left press in SGR, its row the screen's or counted from the prompt's first, as kitty and Ghostty send it, and moves its own cursor, combining marks and its own redraws included: fish moves by code point, and its redraws drop the wrap flags a count would follow. Any other shell gets Left or Right arrows, one per cell, each pressed and let go as the keyboard would, through the key encoder, so DECCKM, kitty's flags and win32-input-mode shape them; the count runs only on the cursor's line, soft wraps joined, past the prompt's cells, which the prompt's link marks, so bash 3.2, which sends no C, moves nothing once its command left the prompt's line. Cells after the last character typed count only up to the cursor, so a click past the end goes to the end; a wide character takes one arrow. Never on the alternate screen. One line only, as Ghostty's `cl=line`. A right prompt or an autosuggestion past the typed text counts as text, as in Ghostty, so a click on it may take the suggestion. The first click of a double click moves the cursor before the word selects, as in Ghostty.
|
||||
- 2026-10-19: a plain click on the line the shell reads moves its cursor there, which a user asked for after 0.1.6, as Ghostty's `cursor-click-to-move`: a press with no modifier, one click, let go in its own cell with no selection, while OSC 133 says the shell reads a line, B since the last A or C, and the program tracks no mouse at the release. A shell whose prompt start says `click_events=1` or `2`, as fish from 4.1 says on its own, hears a click past the prompt's last cell before the cursor, or, where no prompt cell of its own line comes before the input, as under fish's empty prompt, from the first cell of the newest line typed, and never on a prompt cell, itself, a left press in SGR, its row the screen's or counted from the prompt's first, as kitty and Ghostty send it, and moves its own cursor, combining marks and its own redraws included: fish moves by code point, and its redraws drop the wrap flags a count would follow. Any other shell gets Left or Right arrows, one per cell, each pressed and let go as the keyboard would, through the key encoder, so DECCKM, kitty's flags and win32-input-mode shape them; the count runs only on the cursor's line, soft wraps joined, past the prompt's cells, which the prompt's link marks, so bash 3.2, which sends no C, moves nothing once its command left the prompt's line. Cells after the last character typed count only up to the cursor, so a click past the end goes to the end; a wide character takes one arrow. Never on the alternate screen. One line only, as Ghostty's `cl=line`. A right prompt or an autosuggestion past the typed text counts as text, as in Ghostty, so a click on it may take the suggestion. The first click of a double click moves the cursor before the word selects, as in Ghostty.
|
||||
+44
-24
@@ -13,7 +13,7 @@ use gpui::Context;
|
||||
use super::{
|
||||
marks::Click,
|
||||
mouse::Tracking,
|
||||
prompts::{prompted, start},
|
||||
prompts::{Part, part, prompted, start},
|
||||
view::TerminalView,
|
||||
};
|
||||
|
||||
@@ -92,37 +92,57 @@ pub fn arrows<T>(term: &Term<T>, click: Point) -> Option<i32> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The left press, SGR, a shell that takes clicks hears for `click`, its row the screen's or counted from its prompt's first, as kitty and Ghostty send it; none on the prompt or before its end.
|
||||
/// The left press, SGR, a shell that takes clicks hears for `click`, its row the screen's or counted from its prompt's first, as kitty and Ghostty send it; none on a prompt or before the input.
|
||||
pub fn press<T>(term: &Term<T>, click: Point, relative: bool) -> Option<Vec<u8>> {
|
||||
if term.mode().contains(TermMode::ALT_SCREEN) {
|
||||
return None;
|
||||
}
|
||||
let grid = term.grid();
|
||||
let cursor = grid.cursor.point;
|
||||
let first = start(term, cursor.line.0)?;
|
||||
let cell = |line: i32, column: usize| &grid[Line(line)][Column(column)];
|
||||
// The prompt ends at its last cell before the cursor; an empty one, at its first row's start.
|
||||
let end = (first..=cursor.line.0)
|
||||
.flat_map(|line| {
|
||||
let width = if line == cursor.line.0 {
|
||||
cursor.column.0
|
||||
} else {
|
||||
term.columns()
|
||||
};
|
||||
(0..width).map(move |column| (line, column))
|
||||
})
|
||||
.filter(|&(line, column)| prompted(cell(line, column)))
|
||||
.last();
|
||||
let cell = |(line, column): (i32, usize)| &grid[Line(line)][Column(column)];
|
||||
let cursor = (grid.cursor.point.line.0, grid.cursor.point.column.0);
|
||||
// ponytail: scans of the rows on the way; index the terminal's own links if long histories lag.
|
||||
let back = (grid.topmost_line().0..=grid.bottommost_line().0)
|
||||
.rev()
|
||||
.flat_map(|line| (0..term.columns()).rev().map(move |column| (line, column)));
|
||||
let end = (back.clone())
|
||||
.filter(|&at| at < cursor)
|
||||
.find(|&at| prompted(cell(at)));
|
||||
// The newest line typed, its first cell; a shell drawing its prompts again keeps its number.
|
||||
let mut typed = back.filter_map(|at| match part(cell(at)) {
|
||||
Some(Part::Input(line)) => Some((at, line)),
|
||||
_ => None,
|
||||
});
|
||||
let input = typed.next().map(|(last, line)| {
|
||||
let first =
|
||||
(typed.take_while(|&(_, other)| other == line).last()).map_or(last, |(first, _)| first);
|
||||
(first, line)
|
||||
});
|
||||
// An older line typed between a prompt and the input leaves that prompt behind.
|
||||
let older = |end: (i32, usize), (input, line): ((i32, usize), u32)| {
|
||||
(end.0..=input.0)
|
||||
.flat_map(|row| {
|
||||
let from = if row == end.0 { end.1 + 1 } else { 0 };
|
||||
let to = if row == input.0 {
|
||||
input.1
|
||||
} else {
|
||||
term.columns()
|
||||
};
|
||||
(from..to).map(move |column| (row, column))
|
||||
})
|
||||
.any(|at| matches!(part(cell(at)), Some(Part::Input(other)) if other != line))
|
||||
};
|
||||
let at = (click.line.0, click.column.0);
|
||||
let before = end.map_or(at.0 < first, |end| at <= end);
|
||||
if before || prompted(cell(at.0, at.1)) {
|
||||
// Input starts past the prompt's last cell before the cursor, or at its first typed cell where no prompt of its own came before it.
|
||||
let (before, first) = match (end, input) {
|
||||
(Some(end), Some(typed)) if typed.0 > end && older(end, typed) => (at < typed.0, typed.0.0),
|
||||
(Some(end), _) => (at <= end, start(term, end.0)?),
|
||||
(None, Some((input, _))) => (at < input, input.0),
|
||||
(None, None) => return None,
|
||||
};
|
||||
if before || prompted(cell(at)) {
|
||||
return None;
|
||||
}
|
||||
let row = if relative {
|
||||
click.line.0 - first
|
||||
} else {
|
||||
click.line.0
|
||||
} + 1;
|
||||
let row = if relative { at.0 - first } else { at.0 } + 1;
|
||||
if row < 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
+69
-2
@@ -128,7 +128,7 @@ fn a_shell_that_takes_clicks_hears_the_press_where_it_landed() {
|
||||
);
|
||||
let right = term(
|
||||
20,
|
||||
&format!("{marked}ls\x1b7\x1b[1;17H\x1b]133;A\x07~/a\x1b]133;B\x07\x1b8"),
|
||||
&format!("{marked}\x1b[1;17H\x1b]133;A\x07~/a\x1b]133;B\x07\x1b[1;3Hls"),
|
||||
);
|
||||
assert_eq!(
|
||||
sent(&right, click(0, 18), false),
|
||||
@@ -139,9 +139,10 @@ fn a_shell_that_takes_clicks_hears_the_press_where_it_landed() {
|
||||
sent(&right, click(0, 3), false).as_deref(),
|
||||
Some("\x1b[<0;4;1M")
|
||||
);
|
||||
assert_eq!(sent(&right, click(0, 1), false), None, "on the left prompt");
|
||||
let empty = term(
|
||||
20,
|
||||
"\x1b7\x1b[1;17H\x1b]133;A;click_events=1\x07~/a\x1b]133;B\x07\x1b8ls",
|
||||
"\x1b[1;17H\x1b]133;A;click_events=1\x07~/a\x1b]133;B\x07\x1b[1;1Hls",
|
||||
);
|
||||
let left = sent(&empty, click(0, 1), false);
|
||||
assert_eq!(left.as_deref(), Some("\x1b[<0;2;1M"), "only a right prompt");
|
||||
@@ -186,3 +187,69 @@ fn input_reads_the_line_its_prompt_started() {
|
||||
Shell::Reading(Click::Relative)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_prompt_that_prints_nothing_starts_the_input_at_its_first_typed_cell() {
|
||||
let empty = "\x1b]133;A;click_events=1\x07\x1b]133;B\x07";
|
||||
let typed = term(20, &format!("echo hi\r\nhi\r\n{empty}abcdef"));
|
||||
let at = sent(&typed, click(2, 3), false);
|
||||
assert_eq!(at.as_deref(), Some("\x1b[<0;4;3M"), "on the d");
|
||||
let relative = sent(&typed, click(2, 3), true);
|
||||
assert_eq!(
|
||||
relative.as_deref(),
|
||||
Some("\x1b[<0;4;1M"),
|
||||
"from the input's row"
|
||||
);
|
||||
assert_eq!(sent(&typed, click(1, 1), false), None, "output above");
|
||||
assert_eq!(
|
||||
sent(&typed, click(0, 2), false),
|
||||
None,
|
||||
"the line run before"
|
||||
);
|
||||
let moved = term(20, &format!("{empty}abcdef\x1b[6D"));
|
||||
let back = sent(&moved, click(0, 4), false);
|
||||
assert_eq!(
|
||||
back.as_deref(),
|
||||
Some("\x1b[<0;5;1M"),
|
||||
"the cursor at the start"
|
||||
);
|
||||
let blank = term(20, &format!("echo hi\r\n{empty}"));
|
||||
assert_eq!(
|
||||
sent(&blank, click(0, 2), false),
|
||||
None,
|
||||
"nothing typed, nothing after"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_prompt_drawn_again_after_the_typed_text_keeps_its_line() {
|
||||
let marked = "\x1b]133;A;click_events=1\x07";
|
||||
let right = format!("\x1b[1;17H{marked}RIGHT\x1b]133;B\x07\x1b[1;7H");
|
||||
let repainted = term(24, &format!("{marked}\x1b]133;B\x07abcdef{right}"));
|
||||
let at = sent(&repainted, click(0, 3), false);
|
||||
assert_eq!(
|
||||
at.as_deref(),
|
||||
Some("\x1b[<0;4;1M"),
|
||||
"fish's right prompt again"
|
||||
);
|
||||
assert_eq!(
|
||||
sent(&repainted, click(0, 17), false),
|
||||
None,
|
||||
"on that prompt"
|
||||
);
|
||||
let ran = "\x1b]133;C\x07\x1b]133;D;0\x07";
|
||||
let after = format!("$ \x1b]133;B\x07echo hi{ran}\r\nhi\r\n{marked}\x1b]133;B\x07abcdef");
|
||||
let below = term(20, &format!("{marked}{after}"));
|
||||
assert_eq!(sent(&below, click(1, 1), false), None, "the output between");
|
||||
assert_eq!(
|
||||
sent(&below, click(0, 4), false),
|
||||
None,
|
||||
"the line run before"
|
||||
);
|
||||
let typed = sent(&below, click(2, 2), true);
|
||||
assert_eq!(
|
||||
typed.as_deref(),
|
||||
Some("\x1b[<0;3;1M"),
|
||||
"from the input's own row"
|
||||
);
|
||||
}
|
||||
+73
-35
@@ -13,6 +13,24 @@ pub(crate) const LINK: &str = "\u{7f}";
|
||||
/// What a link's id starts with when it carries a prompt's number: the prompt's own link, or a program's inside it.
|
||||
const NUMBERED: char = '\u{7f}';
|
||||
|
||||
/// What ends the id of a link on the line typed after a prompt.
|
||||
const TYPED: char = 'i';
|
||||
|
||||
/// What the terminal's own link stamps: a prompt's cells, by its number, or the line typed after it, OSC 133 B to C, by the commands ended before it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(super) enum Part {
|
||||
Prompt(u32),
|
||||
Input(u32),
|
||||
}
|
||||
|
||||
/// How a mark moves the link: a prompt starts, its input starts, or the command ends it.
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Edge {
|
||||
Prompt,
|
||||
Input,
|
||||
End,
|
||||
}
|
||||
|
||||
/// What ends a prompt's link.
|
||||
const CLOSE: &[u8] = b"\x1b]8;;\x07";
|
||||
|
||||
@@ -53,12 +71,14 @@ pub(crate) struct Prompts {
|
||||
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>,
|
||||
/// The prompt or input open now, from its mark to the next.
|
||||
open: Option<Part>,
|
||||
/// 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,
|
||||
/// Commands ended, C or D, so every prompt a shell draws again on one line shares its input's number.
|
||||
ended: u32,
|
||||
/// What goes to the parser from the last read.
|
||||
out: Vec<u8>,
|
||||
}
|
||||
@@ -116,7 +136,7 @@ impl Prompts {
|
||||
(State::Escape { .. }, _) => {
|
||||
// A full reset clears the link the parser writes with.
|
||||
if byte == b'c' {
|
||||
(self.prompt, self.target) = (None, None);
|
||||
(self.open, self.target) = (None, None);
|
||||
}
|
||||
self.held.push(byte);
|
||||
self.release();
|
||||
@@ -175,7 +195,7 @@ impl Prompts {
|
||||
let length = self.body.len().min(prefix.len());
|
||||
self.body[..length] == prefix[..length]
|
||||
};
|
||||
!self.overflow && (fits(b"133;") || (self.prompt.is_some() && fits(b"8;")))
|
||||
!self.overflow && (fits(b"133;") || (self.open.is_some() && fits(b"8;")))
|
||||
}
|
||||
|
||||
fn release(&mut self) {
|
||||
@@ -186,27 +206,31 @@ impl Prompts {
|
||||
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) {
|
||||
if opens {
|
||||
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.
|
||||
Some(match &self.target {
|
||||
Some(target) => link(self.count, target),
|
||||
None => link(self.count, LINK.as_bytes()),
|
||||
})
|
||||
} else {
|
||||
match (self.prompt.take(), &self.target) {
|
||||
(Some(_), Some(target)) => Some(plain(target)),
|
||||
(Some(_), None) => Some(CLOSE.to_vec()),
|
||||
(None, _) => None,
|
||||
} else if let Some(edge) = self.body.strip_prefix(b"133;").and_then(mark) {
|
||||
// A prompt or input inside a program's link carries its number on that link.
|
||||
let target = self.target.as_deref().unwrap_or(LINK.as_bytes());
|
||||
match (edge, self.open) {
|
||||
(Edge::Prompt, _) => {
|
||||
self.count = self.count.wrapping_add(1);
|
||||
self.open = Some(Part::Prompt(self.count));
|
||||
Some(link(Part::Prompt(self.count), target))
|
||||
}
|
||||
(Edge::Input, Some(Part::Prompt(_))) => {
|
||||
self.open = Some(Part::Input(self.ended));
|
||||
Some(link(Part::Input(self.ended), target))
|
||||
}
|
||||
(Edge::Input, _) => None,
|
||||
(Edge::End, open) => {
|
||||
self.ended = self.ended.wrapping_add(1);
|
||||
self.open = None;
|
||||
open.map(|_| self.target.as_deref().map_or_else(|| CLOSE.to_vec(), plain))
|
||||
}
|
||||
}
|
||||
} else if let Some((opens, target)) = user(&self.body) {
|
||||
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.
|
||||
// Inside a prompt or input a program's link carries its number; its end takes the terminal's link up again.
|
||||
let target = if opens { target } else { LINK.as_bytes() };
|
||||
self.prompt.map(|number| link(number, target))
|
||||
self.open.map(|part| link(part, target))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -232,15 +256,16 @@ impl Prompts {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a mark's body opens a prompt or ends one, as `marks::read` reads its letter; none for neither.
|
||||
fn mark(body: &[u8]) -> Option<bool> {
|
||||
/// How a mark's body moves the link, as `marks::read` reads its letter; none for no mark.
|
||||
fn mark(body: &[u8]) -> Option<Edge> {
|
||||
let (&letter, rest) = body.split_first()?;
|
||||
if !rest.is_empty() && rest[0] != b';' {
|
||||
return None;
|
||||
}
|
||||
match letter {
|
||||
b'A' | b'N' => Some(true),
|
||||
b'B' | b'I' | b'C' | b'D' => Some(false),
|
||||
b'A' | b'N' => Some(Edge::Prompt),
|
||||
b'B' | b'I' => Some(Edge::Input),
|
||||
b'C' | b'D' => Some(Edge::End),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -255,9 +280,13 @@ fn user(body: &[u8]) -> Option<(bool, &[u8])> {
|
||||
Some((opens, target))
|
||||
}
|
||||
|
||||
/// A link to `target` whose id carries prompt `number`.
|
||||
fn link(number: u32, target: &[u8]) -> Vec<u8> {
|
||||
let mut link = format!("\x1b]8;id={NUMBERED}{number};").into_bytes();
|
||||
/// A link to `target` whose id carries `part`'s prompt number.
|
||||
fn link(part: Part, target: &[u8]) -> Vec<u8> {
|
||||
let id = match part {
|
||||
Part::Prompt(number) => format!("{NUMBERED}{number}"),
|
||||
Part::Input(number) => format!("{NUMBERED}{number}{TYPED}"),
|
||||
};
|
||||
let mut link = format!("\x1b]8;id={id};").into_bytes();
|
||||
link.extend_from_slice(target);
|
||||
link.push(0x07);
|
||||
link
|
||||
@@ -271,17 +300,26 @@ fn plain(target: &[u8]) -> Vec<u8> {
|
||||
link
|
||||
}
|
||||
|
||||
/// Whether `cell` belongs to a prompt, its link's id carrying a prompt's number.
|
||||
pub(super) fn prompted(cell: &Cell) -> bool {
|
||||
cell.hyperlink()
|
||||
.is_some_and(|link| link.id().starts_with(NUMBERED))
|
||||
/// The prompt or input `cell` belongs to, as its link's id says.
|
||||
pub(super) fn part(cell: &Cell) -> Option<Part> {
|
||||
let link = cell.hyperlink()?;
|
||||
let number = link.id().strip_prefix(NUMBERED)?;
|
||||
match number.strip_suffix(TYPED) {
|
||||
Some(typed) => typed.parse().ok().map(Part::Input),
|
||||
None => number.parse().ok().map(Part::Prompt),
|
||||
}
|
||||
}
|
||||
|
||||
/// The number of the prompt `row` shows, as a link's id carries it.
|
||||
/// Whether `cell` belongs to a prompt.
|
||||
pub(super) fn prompted(cell: &Cell) -> bool {
|
||||
matches!(part(cell), Some(Part::Prompt(_)))
|
||||
}
|
||||
|
||||
/// The number of the prompt `row` shows.
|
||||
fn prompt(row: &Row<Cell>) -> Option<u32> {
|
||||
row.into_iter().find_map(|cell| {
|
||||
let link = cell.hyperlink()?;
|
||||
link.id().strip_prefix(NUMBERED)?.parse().ok()
|
||||
row.into_iter().find_map(|cell| match part(cell) {
|
||||
Some(Part::Prompt(number)) => Some(number),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+58
-16
@@ -40,7 +40,12 @@ fn target_of(term: &Term<VoidListener>, line: i32, column: usize) -> Option<Stri
|
||||
fn a_mark_becomes_its_prompt_s_numbered_link() {
|
||||
assert_eq!(
|
||||
passed(&[b"\x1b]133;A\x07$ \x1b]133;B\x1b\\x"]),
|
||||
b"\x1b]8;id=\x7f1;\x7f\x07$ \x1b]8;;\x07\x1b\\x"
|
||||
b"\x1b]8;id=\x7f1;\x7f\x07$ \x1b]8;id=\x7f0i;\x7f\x07\x1b\\x"
|
||||
);
|
||||
assert_eq!(
|
||||
passed(&[b"\x1b]133;A\x07$ \x1b]133;B\x07ls\x1b]133;C\x07out"]),
|
||||
b"\x1b]8;id=\x7f1;\x7f\x07$ \x1b]8;id=\x7f0i;\x7f\x07ls\x1b]8;;\x07out",
|
||||
"the input runs from B to C"
|
||||
);
|
||||
assert_eq!(
|
||||
passed(&[b"\x1b]133;A;k=i\x07\x1b]133;D;1\x18"]),
|
||||
@@ -63,7 +68,11 @@ fn the_rewrite_reads_a_mark_as_the_session_hears_it() {
|
||||
"A", "A;k=i", "N", "B", "I", "C;x", "D;1", "D;abc", "D", "", "AB", "L", "P;k=i", "Q", "a",
|
||||
];
|
||||
for body in bodies {
|
||||
let heard = marks::read(body.as_bytes()).map(|mark| matches!(mark, Mark::Prompt(_)));
|
||||
let heard = marks::read(body.as_bytes()).map(|mark| match mark {
|
||||
Mark::Prompt(_) => Edge::Prompt,
|
||||
Mark::Input => Edge::Input,
|
||||
Mark::Output | Mark::Done(_) => Edge::End,
|
||||
});
|
||||
assert_eq!(mark(body.as_bytes()), heard, "{body}");
|
||||
}
|
||||
}
|
||||
@@ -154,14 +163,11 @@ fn a_program_s_link_inside_a_prompt_carries_its_number() {
|
||||
&open.grid()[Line(0)][Column(1)],
|
||||
&open.grid()[Line(0)][Column(2)],
|
||||
);
|
||||
assert!(
|
||||
z.hyperlink()
|
||||
.is_some_and(|link| !link.id().starts_with(NUMBERED))
|
||||
);
|
||||
assert_eq!(part(z), Some(Part::Input(0)), "the input inside it");
|
||||
assert_eq!(
|
||||
target_of(&open, 0, 2).as_deref(),
|
||||
Some("file:///x"),
|
||||
"its own again past the prompt"
|
||||
"its own target past the prompt"
|
||||
);
|
||||
assert!(
|
||||
y.hyperlink()
|
||||
@@ -178,16 +184,12 @@ fn a_long_target_numbers_a_prompt_in_either_order() {
|
||||
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),
|
||||
prompted(&row[Column(1)]) && !prompted(&row[Column(2)]),
|
||||
"the prompt's cell alone: {bytes:?}"
|
||||
);
|
||||
assert_eq!(part(&row[Column(2)]), Some(Part::Input(0)), "{bytes:?}");
|
||||
assert_eq!(target_of(&term, 0, 2).as_deref(), Some(target.as_str()));
|
||||
}
|
||||
}
|
||||
@@ -215,7 +217,7 @@ fn controls_after_an_osc_s_closing_escape_run_where_vte_runs_them() {
|
||||
fn del_and_high_bytes_in_an_escape_go_on_after_it() {
|
||||
assert_eq!(
|
||||
passed(&[b"\x1b\x7f]133;A\x07$\x1b\xff", b"]133;B\x07"]),
|
||||
b"\x1b\x7f]8;id=\x7f1;\x7f\x07$\x1b\xff]8;;\x07"
|
||||
b"\x1b\x7f]8;id=\x7f1;\x7f\x07$\x1b\xff]8;id=\x7f0i;\x7f\x07"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -228,7 +230,7 @@ fn controls_vte_runs_inside_an_escape_go_on_before_the_link() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_prompt_s_cells_carry_its_link_through_sgr_0_and_its_input_none() {
|
||||
fn a_prompt_s_cells_carry_its_link_through_sgr_0_and_its_input_its_own() {
|
||||
let term = parsed(
|
||||
b"x\r\n\x1b]133;A\x07\x1b[31m$\x1b[0m \x1b]133;B\x07ls",
|
||||
8,
|
||||
@@ -237,7 +239,17 @@ 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_of(&term, 1, 1).as_deref(), Some(LINK));
|
||||
assert_eq!(target_of(&term, 1, 2), None, "input");
|
||||
assert_eq!(
|
||||
part(&term.grid()[Line(1)][Column(2)]),
|
||||
Some(Part::Input(0)),
|
||||
"input"
|
||||
);
|
||||
let ran = parsed(
|
||||
b"\x1b]133;A\x07$\x1b]133;B\x07ls\x1b]133;C\x07\r\nout",
|
||||
8,
|
||||
2,
|
||||
);
|
||||
assert_eq!(target_of(&ran, 1, 0), None, "output");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -329,3 +341,33 @@ fn a_prompt_keeps_its_start_through_a_resize() {
|
||||
let top = term.topmost_line().0 + first.expect("prompt 1") as i32;
|
||||
assert_eq!(offset as i32, -top);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_carries_the_commands_ended_before_it() {
|
||||
let line = b"\x1b]133;A\x07$\x1b]133;B\x07ab\x1b]133;A\x07R\x1b]133;B\x07c";
|
||||
let term = parsed(line, 8, 2);
|
||||
let at = |column: usize| part(&term.grid()[Line(0)][Column(column)]);
|
||||
assert_eq!(
|
||||
(at(0), at(1), at(3)),
|
||||
(
|
||||
Some(Part::Prompt(1)),
|
||||
Some(Part::Input(0)),
|
||||
Some(Part::Prompt(2))
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
at(4),
|
||||
Some(Part::Input(0)),
|
||||
"a prompt drawn again keeps the line"
|
||||
);
|
||||
let next = parsed(
|
||||
b"\x1b]133;A\x07$\x1b]133;B\x07a\x1b]133;C\x07\x1b]133;D\x07\x1b]133;A\x07$\x1b]133;B\x07b",
|
||||
8,
|
||||
2,
|
||||
);
|
||||
assert_eq!(
|
||||
part(&next.grid()[Line(0)][Column(3)]),
|
||||
Some(Part::Input(2)),
|
||||
"C and D each end it"
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -71,7 +71,7 @@ What no check reached: no real KWin drew the blur; of Wayland desktops, only Hyp
|
||||
| OSC 8 and plain links on Cmd-press, Ctrl-press off macOS; inside herdr, herdr opens them whole | T09, T45 | `term::links` tests; on macOS with real herdr 0.9.5 and a stand-in `open` on its PATH, Cmd-press on a URL herdr wraps, on its wrapped tail, on an OSC 8 link and on a URL over three rows opened the whole URL and the OSC 8 target, where before the app opened `.../ELYTermin` and nothing; Cmd-hover underlined the long URL across its rows; outside herdr the app opened the URL itself; an OSC 8 link whose label holds `file:///etc/hosts` and whose target is a web page opened the page through herdr, which keeps the target from the terminal while it takes the mouse; Shift-Cmd-press on a plain `file:///etc/hosts` inside herdr the app opened itself, caught by an `NSWorkspace` stand-in | Pass |
|
||||
| Focus reporting, title, scrollback limit, find | T10 | `term::find` tests; round 3 failed at the cap, its fix stands on a red-green test | Pass |
|
||||
| A scrollbar over the history: a drag on its thumb scrolls, whatever the program does with the wheel | T58 | `term::scrollbar` test: the thumb's share and place follow the history and the view's offset, and a place past either end of the track holds at it; under Xorg's dummy screen, after `seq 1 500`, the thumb stood at the bottom right, a drag to the top showed the first prompt and lines 1 to 39, and one back down lines 220 to 259; with mouse tracking on, the wheel went to the program as reports and did not scroll, while a drag on the thumb showed lines 245 to 284 | Pass |
|
||||
| Click to move: a plain click on the line the shell reads, past its prompt, moves its cursor there, the press itself to a shell that takes clicks, else arrows | T59 | `term::marks` test: A's `click_events=1` and `2` read as a press at the screen's row or counted from the prompt; `term::click` tests: a shell that takes clicks hears an SGR left press at the clicked cell, a row below the prompt too, none before the prompt, in the history or on the alternate screen; B reads the line its prompt started; a click on the typed line counts the characters to it, left or right, past the end means the end, a wide character takes one arrow, soft-wrapped rows are one line, trailing spaces count up to the cursor, and a click on the prompt, another line, a line no prompt shows, output without marks or the alternate screen moves nothing; on macOS `shot` ran zsh with a marked prompt: after `echo hello` a click on the `h` logged `moves the cursor -5`, and `X` typed then ran `echo Xhello`; a drag sent nothing, and clicks on the prompt and on output logged `off the input`; round 1 of T59's review found fish 4.9 a code point per Left, so a count put `X` inside `é`, and its redraw dropped the wrap flag a count followed: with fish's own `click_events=1` the press goes to fish, which then typed `echo Xéabc`, and a second click on the redrawn wrapped line put `Y` at row 2, column 3; zsh still counted 4 arrows; a press let go in another cell and one whose program turned on tracking before the release sent nothing; round 2 found a click on fish's `$` sent to fish, which moved its cursor to the start: a press now goes only past the prompt's last cell before the cursor and never on a prompt cell, so clicks on `$` and its space logged `off the input` and `X` still ended `echo helloX`; round 3 found that a prompt whose only cells lie after the cursor, fish's empty left prompt beside a marked right one, crashed the press: with no prompt cell before the cursor the input starts at the prompt's first row, and a click on `o` there put `X` at `echXo` | Pass |
|
||||
| Click to move: a plain click on the line the shell reads, past its prompt, moves its cursor there, the press itself to a shell that takes clicks, else arrows | T59 | `term::marks` test: A's `click_events=1` and `2` read as a press at the screen's row or counted from the prompt; `term::click` tests: a shell that takes clicks hears an SGR left press at the clicked cell, a row below the prompt too, none before the prompt, in the history or on the alternate screen; B reads the line its prompt started; a click on the typed line counts the characters to it, left or right, past the end means the end, a wide character takes one arrow, soft-wrapped rows are one line, trailing spaces count up to the cursor, and a click on the prompt, another line, a line no prompt shows, output without marks or the alternate screen moves nothing; on macOS `shot` ran zsh with a marked prompt: after `echo hello` a click on the `h` logged `moves the cursor -5`, and `X` typed then ran `echo Xhello`; a drag sent nothing, and clicks on the prompt and on output logged `off the input`; round 1 of T59's review found fish 4.9 a code point per Left, so a count put `X` inside `é`, and its redraw dropped the wrap flag a count followed: with fish's own `click_events=1` the press goes to fish, which then typed `echo Xéabc`, and a second click on the redrawn wrapped line put `Y` at row 2, column 3; zsh still counted 4 arrows; a press let go in another cell and one whose program turned on tracking before the release sent nothing; round 2 found a click on fish's `$` sent to fish, which moved its cursor to the start: a press now goes only past the prompt's last cell before the cursor and never on a prompt cell, so clicks on `$` and its space logged `off the input` and `X` still ended `echo helloX`; round 3 found that a prompt whose only cells lie after the cursor, fish's empty left prompt beside a marked right one, crashed the press: with no prompt cell before the cursor the input starts at the prompt's first row, and a click on `o` there put `X` at `echXo`; round 3 found that a prompt that prints nothing left no cells to find the input by, so clicks on fish's typed line did nothing: B now stamps the typed line with a link of its own, numbered by the commands ended, so fish's empty prompt gave `abcXdef`, with a plain or no right prompt too, a click on the output above moved nothing, and fish's right prompt drawn again at each key kept the typed line's number, `echXo`; `term::prompts` test: input carries the commands ended before it, through a prompt drawn again; `term::click` tests: a prompt that prints nothing starts the input at its first typed cell, the output and the line run before refused | Pass |
|
||||
| A bell makes no sound; the row marks it | T10, T12 | T12 review | Pass |
|
||||
|
||||
## 6. Main window and layout
|
||||
|
||||
Reference in new issue
Block a user