T59: 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: while OSC 133 says the shell reads a line, a click that stays in its cell, past the prompt the cursor's line shows, sends Left or Right arrows through the key encoder, one per character, soft wraps joined; never on the alternate screen, under mouse tracking or with a selection

This commit is contained in:
ZacharyZhang-NY committed 2026-10-19 15:18:17 -04:00
1 parent 8ef91855cd
commit 82cfe91810
12 files changed
+234 -4

No files matched your search

+2 -1
View File
@@ -37,7 +37,7 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- `src/pet.rs`: Ely, peeking over the main window's top edge from a window of her own while the settings want her, or up from its bottom edge where no room lies above it; she draws at every frame the display gives her window; `pet/rig` draws her from the icon's paths in layers of one color, each moved by a matrix of its own, as the films rig her; `pet/look` reads her mood from the sessions' marks and her pose at each moment of it, `look/curves` the timings of her moves; and `pet/macos`, `pet/windows` and `pet/x11` keep her window on the main window's edge where the platform does not. - `src/pet.rs`: Ely, peeking over the main window's top edge from a window of her own while the settings want her, or up from its bottom edge where no room lies above it; she draws at every frame the display gives her window; `pet/rig` draws her from the icon's paths in layers of one color, each moved by a matrix of its own, as the films rig her; `pet/look` reads her mood from the sessions' marks and her pose at each moment of it, `look/curves` the timings of her moves; and `pet/macos`, `pet/windows` and `pet/x11` keep her window on the main window's edge where the platform does not.
- `src/settings`: the model, its ranges, and `SettingsStore`, the global that loads, checks and saves `settings.json`; `window` is the settings window, `window/appearance` its Appearance page, `window/terminal` its Terminal page. `tests/fonts.rs` brings its own main: gpui builds its Mac platform only on the main thread. - `src/settings`: the model, its ranges, and `SettingsStore`, the global that loads, checks and saves `settings.json`; `window` is the settings window, `window/appearance` its Appearance page, `window/terminal` its Terminal page. `tests/fonts.rs` brings its own main: gpui builds its Mac platform only on the main thread.
- `src/themes`: built-in terminal themes, the chrome palette each maps to, and `apply`, which dresses the app for the system's appearance; `omarchy`, on Linux, wears Omarchy 4's current theme and follows its switches. - `src/themes`: built-in terminal themes, the chrome palette each maps to, and `apply`, which dresses the app for the system's appearance; `omarchy`, on Linux, wears Omarchy 4's current theme and follows its switches.
- `src/term`: the terminal engine. `pty` starts a program through alacritty's event loop; `tap` wraps the pty so its reader scans OSC 0, 2, 7, 9, 99, 133, 777 and 7501, the title stack's CSI 22 t and 23 t, full resets, the mouse modes and, on Windows, win32-input-mode (`tap/modes`) before the parser; `titles` keeps the title a session shows, read in the output's order; `marks` reads OSC 133's prompt marks; `status` reads OSC 7501 and keeps a terminal's records; `notify` reads OSC 9, 777 and 99, the notifications and OSC 9;4's progress, and gathers OSC 99's chunks; `reports` is the view's side of what programs report: it keeps OSC 7501's records, maps OSC 9;4, answers OSC 99's query and alive, and posts the system notifications its session posts; `prompts` turns each mark into an OSC 8 link the parser keeps in a prompt's cells, and finds the prompt to jump to; `herdr` follows herdr's focused pane and its agents' states through herdr's socket while herdr's client runs in the session, and tells the view herdr leads, so herdr opens its links: `herdr/unix` finds the client leading the pty's foreground, `herdr/windows` under the session's shell, and asks its named pipe. `unix` starts the program itself, so a failed `chdir` fails the start; Windows uses alacritty's ConPTY. `shell` writes the integration scripts at start and builds each shell's launch. `frame` reads the grid into styled rows, `boxes` turns box drawing, block elements and powerline arrows into rects and polygons (`boxes/outline` strokes curves and diagonals into polygons), `ink` resolves colors, `paint` draws rows, decorations and the cursor, `view` is the entity: pty events, metrics, blink, resize. `zoom` holds the points Bigger and Smaller add to the Size setting, unsaved. `keys` turns key events into bytes, the kitty protocol and, in `keys/legacy`, xterm's encoding, with no gpui types; `input` is the view's key listeners, the bindings that keep Tab and F6 from Ely's focus moves, and its IME handler; `mac` reads the Option side, the repeat flag and the key code of AppKit's current event, and the key's own character from the layout, as gpui reads key names; it keeps held keys repeating, and hands Command key-ups to the focused view. `win32` sends a key as Windows' own record, win32-input-mode, and finds a key's own character in Windows' layout. `mouse` encodes mouse reports, with no gpui types; `links` finds the link at a cell; `pointer` is the view's mouse listeners: selection, reports, wheel, links, copy and paste. `scrollbar` is the thumb over the history that a drag moves through it. `find` is the find box: Ely's `FindWidget`, and Ely's `find_all` over the grid's shown text. - `src/term`: the terminal engine. `pty` starts a program through alacritty's event loop; `tap` wraps the pty so its reader scans OSC 0, 2, 7, 9, 99, 133, 777 and 7501, the title stack's CSI 22 t and 23 t, full resets, the mouse modes and, on Windows, win32-input-mode (`tap/modes`) before the parser; `titles` keeps the title a session shows, read in the output's order; `marks` reads OSC 133's prompt marks; `status` reads OSC 7501 and keeps a terminal's records; `notify` reads OSC 9, 777 and 99, the notifications and OSC 9;4's progress, and gathers OSC 99's chunks; `reports` is the view's side of what programs report: it keeps OSC 7501's records, maps OSC 9;4, answers OSC 99's query and alive, and posts the system notifications its session posts; `prompts` turns each mark into an OSC 8 link the parser keeps in a prompt's cells, and finds the prompt to jump to; `herdr` follows herdr's focused pane and its agents' states through herdr's socket while herdr's client runs in the session, and tells the view herdr leads, so herdr opens its links: `herdr/unix` finds the client leading the pty's foreground, `herdr/windows` under the session's shell, and asks its named pipe. `unix` starts the program itself, so a failed `chdir` fails the start; Windows uses alacritty's ConPTY. `shell` writes the integration scripts at start and builds each shell's launch. `frame` reads the grid into styled rows, `boxes` turns box drawing, block elements and powerline arrows into rects and polygons (`boxes/outline` strokes curves and diagonals into polygons), `ink` resolves colors, `paint` draws rows, decorations and the cursor, `view` is the entity: pty events, metrics, blink, resize. `zoom` holds the points Bigger and Smaller add to the Size setting, unsaved. `keys` turns key events into bytes, the kitty protocol and, in `keys/legacy`, xterm's encoding, with no gpui types; `input` is the view's key listeners, the bindings that keep Tab and F6 from Ely's focus moves, and its IME handler; `mac` reads the Option side, the repeat flag and the key code of AppKit's current event, and the key's own character from the layout, as gpui reads key names; it keeps held keys repeating, and hands Command key-ups to the focused view. `win32` sends a key as Windows' own record, win32-input-mode, and finds a key's own character in Windows' layout. `mouse` encodes mouse reports, with no gpui types; `links` finds the link at a cell; `pointer` is the view's mouse listeners: selection, reports, wheel, links, copy and paste. `scrollbar` is the thumb over the history that a drag moves through it. `click` turns a click on the line the shell reads into arrows to it. `find` is the find box: Ely's `FindWidget`, and Ely's `find_all` over the grid's shown text.
- `assets/shell-integration`: ELYTerminal's own scripts (not Ghostty's, which are GPL): zsh through `ZDOTDIR`, bash through `--rcfile`, fish through `XDG_DATA_DIRS`, PowerShell through `-NoExit -Command`. Each prompt sends OSC 7 and, except in fish, whose `fish_title` stays, a title; each marks prompts and commands with OSC 133 where the shell does not. - `assets/shell-integration`: ELYTerminal's own scripts (not Ghostty's, which are GPL): zsh through `ZDOTDIR`, bash through `--rcfile`, fish through `XDG_DATA_DIRS`, PowerShell through `-NoExit -Command`. Each prompt sends OSC 7 and, except in fish, whose `fish_title` stays, a title; each marks prompts and commands with OSC 133 where the shell does not.
- `assets/fonts`: JetBrains Mono 2.304's bold and italic faces (OFL; Ely bundles regular), and Nerd Fonts 3.5.1's JetBrains Mono NF Mono Regular, which draws icons a font lacks. - `assets/fonts`: JetBrains Mono 2.304's bold and italic faces (OFL; Ely bundles regular), and Nerd Fonts 3.5.1's JetBrains Mono NF Mono Regular, which draws icons a font lacks.
- `assets/themes`: Ghostty theme files copied verbatim from iTerm2-Color-Schemes (MIT, its `LICENSE` beside them). - `assets/themes`: Ghostty theme files copied verbatim from iTerm2-Color-Schemes (MIT, its `LICENSE` beside them).
@@ -114,3 +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: 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: 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: 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, that stays in its cell and leaves no selection sends Left or Right arrows, one per character, each pressed and let go as the keyboard would, through the key encoder, so DECCKM, kitty's flags and win32-input-mode shape them. It runs only while OSC 133 says the shell reads a line, B since the last A or C, and only when the cursor's line, soft wraps joined, shows the prompt's cells before the cursor, which the prompt's link marks, and the click lies past them; 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 or while the program tracks the mouse, whose press it hears instead. One line only, as Ghostty's `cl=line`: a continuation line of a multi-line command is another 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.
+1
View File
@@ -57,6 +57,7 @@ Ghostty is the bar for what the terminal does. ELYTerminal owns its engine on `a
- Keys: xterm encoding with modifiers, application cursor mode, Option as Alt on macOS, the kitty keyboard protocol as the program sets its flags; a key Shift makes a symbol of goes as its own key with Shift, the symbol its alternate, as Ghostty sends it, on macOS and Windows. On Windows keys go as win32-input-mode records while ConPTY asks, as Windows Terminal sends them. Keypad keys type their characters, as Ghostty's do with mode 1035 on by default. - Keys: xterm encoding with modifiers, application cursor mode, Option as Alt on macOS, the kitty keyboard protocol as the program sets its flags; a key Shift makes a symbol of goes as its own key with Shift, the symbol its alternate, as Ghostty sends it, on macOS and Windows. On Windows keys go as win32-input-mode records while ConPTY asks, as Windows Terminal sends them. Keypad keys type their characters, as Ghostty's do with mode 1035 on by default.
- IME: marked text drawn at the cursor; committed text sent to the program. - IME: marked text drawn at the cursor; committed text sent to the program.
- Mouse: select by character, word (double press) and line (triple press); Shift extends; mouse reporting (X10, normal, button, any; SGR and UTF-8); wheel scrolls back, or reports, or sends arrows on the alternate screen. - Mouse: select by character, word (double press) and line (triple press); Shift extends; mouse reporting (X10, normal, button, any; SGR and UTF-8); wheel scrolls back, or reports, or sends arrows on the alternate screen.
- Click to move: while the shell reads a line, as its OSC 133 marks say, a plain click on that line, past its prompt, moves the shell's cursor there with arrow keys, as Ghostty's `cursor-click-to-move`.
- Clipboard: copy, paste (bracketed when asked), copy on select per setting, OSC 52 writes. - Clipboard: copy, paste (bracketed when asked), copy on select per setting, OSC 52 writes.
- Links: OSC 8 and plain URLs; Cmd-press (Ctrl-press off macOS) opens them. Inside herdr, while it takes the mouse, herdr opens them, web links only, as herdr does, whole where its pane wraps them, and underlines them while the key is held; with Shift the terminal opens what it sees. - Links: OSC 8 and plain URLs; Cmd-press (Ctrl-press off macOS) opens them. Inside herdr, while it takes the mouse, herdr opens them, web links only, as herdr does, whole where its pane wraps them, and underlines them while the key is held; with Shift the terminal opens what it sees.
- Focus reporting, title (OSC 0/2), scrollback limit per setting, find in scrollback (Ely's `FindWidget`). - Focus reporting, title (OSC 0/2), scrollback limit per setting, find in scrollback (Ely's `FindWidget`).
+12
View File
@@ -149,3 +149,15 @@ Progress truth. Top to bottom, one at a time. A task is ticked only after its co
## 31. Release ## 31. Release
- [x] T57 Release v0.1.6 with T55, T56 and T58: version 0.1.6; the macOS disk image, the Windows zip and the Linux tarball, each from its script; the notes list what changed. - [x] T57 Release v0.1.6 with T55, T56 and T58: version 0.1.6; the macOS disk image, the Windows zip and the Linux tarball, each from its script; the notes list what changed.
## 32. User feedback after 0.1.6
- [ ] T59 A plain click on the line typed at the shell prompt moves the cursor there, which a user asked for after 0.1.6, as Ghostty's `cursor-click-to-move`: while the shell's OSC 133 marks say it reads a line, a click that does not drag sends Left or Right arrows, one per character, to the clicked cell of the cursor's line, soft wraps joined; never on the alternate screen, under mouse tracking, with a selection, or before the prompt's end.
- [ ] T60 Fallback fonts, which a user asked for after 0.1.6: `font_fallbacks` in the settings, a list of installed families tried in order for a character Font family lacks, CJK among them; Settings › Appearance lists them under Font family, each an Ely `FontPicker` with a remove button, and an Add button; each saves only when gpui loads it.
- [ ] T61 On Windows the IME's candidate window stays at the cursor, which a user found jumping after 0.1.6: find why it moves, fix the cause, and log what the input handler tells the IME.
- [ ] T62 Ely's body runs on below the edge she holds, which a user asked for after 0.1.6, where a hop lifted her off the window and left a gap under her collar: her shirt continues down, hidden behind the edge, so a hop shows more of her and never a gap.
- [ ] T63 A double-click on a session's title renames it, which a user asked for after 0.1.6: the row's title turns into a field, Enter keeps the name, Escape drops it; the name shows in the row, the terminal's header and the window's title over the program's own until renamed again, and an empty name gives the title back to the program; it lives as long as the session.
## 33. Release
- [ ] T64 Release v0.1.7 with T59 to T63: version 0.1.7; the macOS disk image, the Windows zip and the Linux tarball, each from its script; the notes list what changed in English and Chinese.
+89
View File
@@ -0,0 +1,89 @@
//! A click on the line the shell reads moves its cursor there with arrows, as Ghostty's `cursor-click-to-move`.
use alacritty_terminal::{
grid::Dimensions,
index::{Column, Line, Point},
term::{
Term, TermMode,
cell::{Cell, Flags},
},
};
use gpui::Context;
use super::{prompts::prompted, view::TerminalView};
/// Arrows from the cursor to `click`, right positive; none when `click` lies off the input after the cursor's prompt, soft wraps joined.
pub fn arrows<T>(term: &Term<T>, click: Point) -> Option<i32> {
if term.mode().contains(TermMode::ALT_SCREEN) {
return None;
}
let grid = term.grid();
let columns = grid.columns();
let cursor = grid.cursor.point;
let wraps = |line: i32| {
grid[Line(line)][Column(columns - 1)]
.flags
.contains(Flags::WRAPLINE)
};
let mut first = cursor.line.0;
while first > grid.topmost_line().0 && wraps(first - 1) {
first -= 1;
}
let mut last = cursor.line.0;
while last < grid.bottommost_line().0 && wraps(last) {
last += 1;
}
if !(first..=last).contains(&click.line.0) {
return None;
}
let cells: Vec<&Cell> = (first..=last)
.flat_map(|line| (0..columns).map(move |column| &grid[Line(line)][Column(column)]))
.collect();
let at = |point: Point| (point.line.0 - first) as usize * columns + point.column.0;
let (here, there) = (at(cursor), at(click));
// Input starts past the prompt the cursor's line shows before it.
let start = cells[..here].iter().rposition(|cell| prompted(cell))? + 1;
if there < start {
return None;
}
let inked = |cell: &&Cell| !prompted(cell) && !matches!(cell.c, ' ' | '\t');
let end = cells[start..]
.iter()
.rposition(inked)
.map_or(here, |last| here.max(start + last + 1));
let mut target = there.min(end);
if cells[target].flags.contains(Flags::WIDE_CHAR_SPACER) {
target -= 1;
}
let spacer = Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER;
let typed = |cell: &&&Cell| !prompted(cell) && !cell.flags.intersects(spacer);
let count = |from: usize, to: usize| cells[from..to].iter().filter(typed).count() as i32;
Some(if target >= here {
count(here, target)
} else {
-count(target, here)
})
}
impl TerminalView {
/// Moves the shell's cursor to `click`, a press and release of an arrow per character, while the shell reads a line.
pub(super) fn click_to_move(&mut self, click: Point, cx: &mut Context<Self>) {
let pid = self.pty.pid;
if !self.reading {
log::debug!("terminal {pid}: a click at {click:?}, no line read");
return;
}
let Some(count) = arrows(&self.pty.term.lock(), click) else {
log::debug!("terminal {pid}: a click at {click:?}, off the input");
return;
};
log::debug!("terminal {pid}: a click at {click:?} moves the cursor {count}");
let arrow = if count < 0 { "left" } else { "right" };
for _ in 0..count.unsigned_abs() {
self.tap_key(arrow, cx);
}
}
}
#[cfg(test)]
mod tests;
+91
View File
@@ -0,0 +1,91 @@
use alacritty_terminal::{
event::VoidListener,
term::{Config, test::TermSize},
vte::ansi::Processor,
};
use super::*;
use crate::term::prompts::Prompts;
const PROMPT: &str = "\x1b]133;A\x07$ \x1b]133;B\x07";
/// A terminal `columns` wide after `text` passed the prompts' rewrite, as the tap passes it.
fn term(columns: usize, text: &str) -> Term<VoidListener> {
let mut term = Term::new(Config::default(), &TermSize::new(columns, 4), VoidListener);
let mut parser: Processor = Processor::new();
parser.advance(&mut term, Prompts::default().pass(text.as_bytes()));
term
}
fn click(line: i32, column: usize) -> Point {
Point::new(Line(line), Column(column))
}
#[test]
fn a_click_on_the_typed_line_counts_the_characters_to_it() {
let typed = term(20, &format!("{PROMPT}echo hello"));
assert_eq!(arrows(&typed, click(0, 7)), Some(-5), "back to hello");
assert_eq!(
arrows(&typed, click(0, 2)),
Some(-10),
"the first character"
);
assert_eq!(
arrows(&typed, click(0, 12)),
Some(0),
"the cursor's own cell"
);
let moved = term(20, &format!("{PROMPT}echo hello\x1b[5D"));
assert_eq!(arrows(&moved, click(0, 11)), Some(4), "on to the last o");
assert_eq!(
arrows(&moved, click(0, 18)),
Some(5),
"past the end, the end"
);
}
#[test]
fn a_click_off_the_input_moves_nothing() {
let typed = term(20, &format!("{PROMPT}ls"));
assert_eq!(arrows(&typed, click(0, 0)), None, "on the prompt");
assert_eq!(arrows(&typed, click(0, 1)), None, "on the prompt's space");
assert_eq!(arrows(&typed, click(2, 3)), None, "another line");
let ran = term(20, &format!("{PROMPT}cat\r\nhello"));
assert_eq!(arrows(&ran, click(1, 2)), None, "a line no prompt shows");
let output = term(20, "$ plain text");
assert_eq!(arrows(&output, click(0, 3)), None, "no marks at all");
let full = term(20, &format!("{PROMPT}vim\x1b[?1049h"));
assert_eq!(arrows(&full, click(0, 3)), None, "the alternate screen");
}
#[test]
fn a_wide_character_takes_one_arrow() {
let wide = term(20, &format!("{PROMPT}中文ab"));
assert_eq!(arrows(&wide, click(0, 3)), Some(-4), "the right half of 中");
assert_eq!(arrows(&wide, click(0, 4)), Some(-3), "文");
assert_eq!(arrows(&wide, click(0, 7)), Some(-1), "b");
}
#[test]
fn soft_wrapped_rows_are_one_line() {
let wrapped = term(10, &format!("{PROMPT}abcdefghijkl"));
assert_eq!(wrapped.grid().cursor.point, click(1, 4));
assert_eq!(arrows(&wrapped, click(0, 3)), Some(-11), "back to b");
let back = term(10, &format!("{PROMPT}abcdefghijkl\x1b[3D"));
assert_eq!(arrows(&back, click(0, 3)), Some(-8), "up to b");
assert_eq!(arrows(&back, click(1, 3)), Some(2), "on to l");
}
#[test]
fn trailing_spaces_count_only_up_to_the_cursor() {
let spaced = term(20, &format!("{PROMPT}ls "));
assert_eq!(arrows(&spaced, click(0, 15)), Some(0));
assert_eq!(arrows(&spaced, click(0, 4)), Some(-2));
}
#[test]
fn a_prompt_over_two_lines_keeps_its_input_on_the_last() {
let two = term(20, "\x1b]133;A\x07~/src\r\n> \x1b]133;B\x07make");
assert_eq!(arrows(&two, click(1, 2)), Some(-4));
assert_eq!(arrows(&two, click(0, 3)), None, "the prompt's first line");
}
+12
View File
@@ -121,6 +121,18 @@ impl TerminalView {
} }
} }
/// Presses and lets go of `name` without modifiers, as the keyboard would.
pub(super) fn tap_key(&mut self, name: &str, cx: &mut Context<Self>) {
let keystroke = Keystroke {
modifiers: Default::default(),
key: name.into(),
key_char: None,
};
let facts = Facts::default();
let record = self.send(&keystroke, Action::Press, facts, false, cx) == Some(true);
self.send(&keystroke, Action::Release, facts, record, cx);
}
/// Sends what `keystroke` means to the program: none when it is text the input method types, else whether it went as Windows' record; `pressed` says that of a held key's press. /// Sends what `keystroke` means to the program: none when it is text the input method types, else whether it went as Windows' record; `pressed` says that of a held key's press.
fn send( fn send(
&mut self, &mut self,
+1 -1
View File
@@ -50,7 +50,7 @@ const LEFT_OPTION: u64 = 0x20;
const RIGHT_OPTION: u64 = 0x40; const RIGHT_OPTION: u64 = 0x40;
/// What gpui drops from the key event AppKit is handling. /// What gpui drops from the key event AppKit is handling.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug, Default)]
pub struct KeyFacts { pub struct KeyFacts {
pub left_option: bool, pub left_option: bool,
pub right_option: bool, pub right_option: bool,
+1
View File
@@ -1,4 +1,5 @@
mod boxes; mod boxes;
mod click;
mod find; mod find;
pub mod frame; pub mod frame;
mod herdr; mod herdr;
+12
View File
@@ -28,6 +28,8 @@ pub struct Pointer {
pub cell: Size<Pixels>, pub cell: Size<Pixels>,
/// The left button is down and drawing a selection. /// The left button is down and drawing a selection.
selecting: bool, selecting: bool,
/// A plain press's cell, until it drags off it; its release may move the shell's cursor.
click: Option<Cell>,
/// Buttons whose press went to the program. /// Buttons whose press went to the program.
pressed: Vec<Button>, pressed: Vec<Button>,
/// The cell of the last report; motion inside it sends nothing. /// The cell of the last report; motion inside it sends nothing.
@@ -149,6 +151,8 @@ impl TerminalView {
if event.button != MouseButton::Left { if event.button != MouseButton::Left {
return; return;
} }
let plain = event.click_count == 1 && !event.modifiers.modified();
self.pointer.click = plain.then_some(point);
let pid = self.pty.pid; let pid = self.pty.pid;
let mut term = self.pty.term.lock(); let mut term = self.pty.term.lock();
match ( match (
@@ -183,6 +187,9 @@ impl TerminalView {
) { ) {
let (cell, point, side) = self.cell_at(event.position); let (cell, point, side) = self.cell_at(event.position);
if self.pointer.selecting && event.pressed_button == Some(MouseButton::Left) { if self.pointer.selecting && event.pressed_button == Some(MouseButton::Left) {
if self.pointer.click.is_some_and(|click| click != point) {
self.pointer.click = None;
}
if let Some(selection) = self.pty.term.lock().selection.as_mut() { if let Some(selection) = self.pty.term.lock().selection.as_mut() {
log::trace!("terminal {}: the selection runs to {point:?}", self.pty.pid); log::trace!("terminal {}: the selection runs to {point:?}", self.pty.pid);
selection.update(point, side); selection.update(point, side);
@@ -207,6 +214,10 @@ impl TerminalView {
) { ) {
if event.button == MouseButton::Left { if event.button == MouseButton::Left {
self.end_selection(cx); self.end_selection(cx);
let click = self.pointer.click.take();
if let Some(click) = click.filter(|_| self.selected().is_none()) {
self.click_to_move(click, cx);
}
} }
if let Some(released) = button(event.button) { if let Some(released) = button(event.button) {
let (cell, ..) = self.cell_at(event.position); let (cell, ..) = self.cell_at(event.position);
@@ -217,6 +228,7 @@ impl TerminalView {
/// Ends the mouse's gesture as releases would: a hidden terminal hears none. /// Ends the mouse's gesture as releases would: a hidden terminal hears none.
pub fn let_go(&mut self, cx: &mut Context<Self>) { pub fn let_go(&mut self, cx: &mut Context<Self>) {
self.end_selection(cx); self.end_selection(cx);
self.pointer.click = None;
for held in self.pointer.pressed.clone() { for held in self.pointer.pressed.clone() {
log::debug!("terminal {}: hidden with {held:?} down", self.pty.pid); log::debug!("terminal {}: hidden with {held:?} down", self.pty.pid);
let cell = self let cell = self
+6
View File
@@ -271,6 +271,12 @@ fn plain(target: &[u8]) -> Vec<u8> {
link 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 number of the prompt `row` shows, as a link's id carries it. /// The number of the prompt `row` shows, as a link's id carries it.
fn prompt(row: &Row<Cell>) -> Option<u32> { fn prompt(row: &Row<Cell>) -> Option<u32> {
row.into_iter().find_map(|cell| { row.into_iter().find_map(|cell| {
+6 -2
View File
@@ -85,6 +85,8 @@ pub struct TerminalView {
pub(super) ended: Option<i32>, pub(super) ended: Option<i32>,
/// A command started, as OSC 133 C says, since the last one ended. /// A command started, as OSC 133 C says, since the last one ended.
pub(super) ran: bool, pub(super) ran: bool,
/// The shell reads a line: OSC 133 B came, and no A or C since.
pub(super) reading: bool,
/// The system notifications it posted. /// The system notifications it posted.
pub(super) notices: Notices, pub(super) notices: Notices,
/// The program's title; empty while it names none. /// The program's title; empty while it names none.
@@ -146,6 +148,7 @@ impl TerminalView {
records: Records::default(), records: Records::default(),
ended: None, ended: None,
ran: false, ran: false,
reading: false,
notices: Notices::default(), notices: Notices::default(),
title: String::new(), title: String::new(),
front: true, front: true,
@@ -274,11 +277,12 @@ impl TerminalView {
PtyEvent::Mark(Mark::Done(status)) => self.finished(status, cx), PtyEvent::Mark(Mark::Done(status)) => self.finished(status, cx),
// A new prompt ends what was working, waiting or idle; done and error stay. // A new prompt ends what was working, waiting or idle; done and error stay.
PtyEvent::Mark(Mark::Prompt) => { PtyEvent::Mark(Mark::Prompt) => {
self.reading = false;
self.records.prompt(); self.records.prompt();
self.restate(cx); self.restate(cx);
} }
PtyEvent::Mark(Mark::Output) => self.ran = true, PtyEvent::Mark(Mark::Output) => (self.ran, self.reading) = (true, false),
PtyEvent::Mark(_) => {} PtyEvent::Mark(Mark::Input) => self.reading = true,
PtyEvent::Status(status) => { PtyEvent::Status(status) => {
self.status(status); self.status(status);
self.restate(cx); self.restate(cx);
+1
View File
@@ -71,6 +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 | | 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 | | 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 | | 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 with arrows | T59 | `term::click` tests: 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` | Pass |
| A bell makes no sound; the row marks it | T10, T12 | T12 review | Pass | | A bell makes no sound; the row marks it | T10, T12 | T12 review | Pass |
## 6. Main window and layout ## 6. Main window and layout