T66: files dropped on the terminal type their paths, which a user asked for after 0.1.6, as macOS Terminal does: each escaped for the shell, or quoted on Windows when it holds a space, followed by a space, sent as a paste; a path not in UTF-8 or holding a control character stops the drop; the font code moves from view.rs to fonts.rs, which view.rs needed room for; shot plays a drop with drop@x,y
This commit is contained in:
10 files changed
+227
-76
No files matched your search
@@ -22,6 +22,7 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
|
||||
- `snap` writes each child window's picture at that moment, `PNG.childN.snapK.png`, so a child that moves shows its frames;
|
||||
- `activate` and `deactivate` the app, which takes real keystrokes while active: keep the two specs adjacent. macOS may delay or refuse the activation; each `shot: typed` line says whether the app was active;
|
||||
- `to:settings` and `to:main` aim the specs after them, and the picture, at that window, which becomes the app's key window, since AppKit sends keys there; the app stays inactive, and gpui sends an inactive window no focus or blur events, so press Enter to commit a field. AppKit gives an inactive app's key that a menu item holds to the menu, and gpui sends the item's action to the main window, which an inactive app lacks: only an app-wide action runs, such as Quit, Settings or the font's size;
|
||||
- `drop@x,y` drops the files `SHOT_DROP` names, one a line, as gpui's view hands a drop on, a move, a release and the drag's end, 120 ms apart;
|
||||
- mouse gestures handed to gpui's view, at points from the content's top-left, modifiers as for keys: `click@x,y`, `click2@`, `click3@`, `rclick@`, `mclick@`, `drag@x,y@x,y`, `move@x,y`, the steps `ldown@`, `lup@`, `ldrag@`, `rdown@`, `rup@`, `mdown@`, `mup@`, and `scrollup@x,y@lines`, `scrolldown@x,y@lines`.
|
||||
- `scripts/check.sh`: fmt, clippy `-D warnings`, tests, the 500-line rule.
|
||||
- `scripts/linux.sh ARGS` runs `cargo ARGS` on the working tree in a Linux container, the build kept in Docker volumes; `scripts/linux.sh test --locked` runs the tests as a user, not root, as CI does. The tree goes in as a tar stream, every file dated now: Colima's shared folder can show the VM a file as it was before an edit, and cargo judges by date, so a tree older than the last build would reuse that build. It needs a Docker daemon: on macOS, Colima (`colima start`).
|
||||
@@ -37,7 +38,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/settings`: the model, its ranges, and `SettingsStore`, the global that loads, checks and saves `settings.json`; `window` is the settings window, `window/appearance` its Appearance page, `window/terminal` its Terminal page. `tests/fonts.rs` brings its own main: gpui builds its Mac platform only on the main thread.
|
||||
- `src/themes`: built-in terminal themes, the chrome palette each maps to, and `apply`, which dresses the app for the system's appearance; `omarchy`, on Linux, wears Omarchy 4's current theme and follows its switches.
|
||||
- `src/term`: the terminal engine. `pty` starts a program through alacritty's event loop; `tap` wraps the pty so its reader scans OSC 0, 2, 7, 9, 99, 133, 777 and 7501, the title stack's CSI 22 t and 23 t, full resets, 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.
|
||||
- `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. `drop` types the paths of files dropped on the terminal. `fonts` loads the terminal's font and measures its cell. `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/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).
|
||||
@@ -115,3 +116,4 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
|
||||
- 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 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, known by its number, not its place, since a prompt drawn at the screen's top leaves older lines below it, down to that line's last row or the cursor's, 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.
|
||||
- 2026-10-19: files dropped on the terminal type their paths, which a user asked for after 0.1.6, as macOS Terminal does: each path followed by a space, the paths of one drop in its order, sent as a paste, bracketed when the program asked. On macOS and Linux each character a shell reads specially, Ghostty's list, gets a backslash; on Windows a path holding a space is wrapped in double quotes, as Windows Terminal wraps it. A path not in UTF-8, or holding a control character, which would act as it is typed, stops the drop, logged. gpui hands a drop to the hitbox under the pointer only while the last input was not the keyboard, and a file drag counts as no input of its own: after typing, a drop with no pointer move over the window since does nothing, and nothing in ELYTerminal sees it, which the owner chose to leave so. `shot`'s `drop@x,y` plays a drop through `Window::dispatch_event`, so AppKit's drag session itself goes untested.
|
||||
@@ -59,6 +59,7 @@ Ghostty is the bar for what the terminal does. ELYTerminal owns its engine on `a
|
||||
- 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, as Ghostty's `cursor-click-to-move`: a shell that takes clicks, as fish does, hears the click; others get arrow keys.
|
||||
- Clipboard: copy, paste (bracketed when asked), copy on select per setting, OSC 52 writes.
|
||||
- Drop: files dropped on the terminal type their paths at the cursor, as macOS Terminal does, each escaped for the shell, or quoted on Windows, and followed by a space.
|
||||
- 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`).
|
||||
- A scrollbar over the history, along the terminal's right edge: a drag on its thumb scrolls, whatever the program does with the wheel.
|
||||
|
||||
@@ -69,6 +69,20 @@ fn main() {
|
||||
aim = settings.expect("a settings window");
|
||||
capture::front(number(aim, cx)).expect("the window on screen");
|
||||
}
|
||||
spec if spec.starts_with("drop@") => {
|
||||
let paths =
|
||||
std::env::var("SHOT_DROP").expect("SHOT_DROP names the paths");
|
||||
let event = typing::drop(&spec[5..], &paths).expect("a drop");
|
||||
// A drag moves over frames before it lands.
|
||||
for step in event {
|
||||
aim.update(cx, |_, window, cx| window.dispatch_event(step, cx))
|
||||
.expect("the window lives");
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(120))
|
||||
.await;
|
||||
}
|
||||
log::info!("shot: dropped {paths:?} at {}", &spec[5..]);
|
||||
}
|
||||
spec => typing::post(spec, number(aim, cx)).expect("a key event"),
|
||||
}
|
||||
if spec.starts_with("to:") {
|
||||
|
||||
@@ -314,3 +314,20 @@ fn key(spec: &str, number: u32, kinds: &[u64]) -> anyhow::Result<()> {
|
||||
log::info!("shot: typed {spec}, the app {state}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A file drop of `paths`, one a line, at `at`, `x,y` from the content's top-left, as gpui's AppKit view sends it.
|
||||
pub fn drop(at: &str, paths: &str) -> anyhow::Result<[gpui::PlatformInput; 4]> {
|
||||
use gpui::{ExternalPaths, FileDropEvent, PlatformInput, point, px};
|
||||
let (x, y) = at
|
||||
.split_once(',')
|
||||
.with_context(|| format!("a point in {at}"))?;
|
||||
let position = point(px(x.parse()?), px(y.parse()?));
|
||||
let paths = ExternalPaths(paths.lines().map(Into::into).collect());
|
||||
Ok([
|
||||
FileDropEvent::Entered { position, paths },
|
||||
FileDropEvent::Pending { position },
|
||||
FileDropEvent::Submit { position },
|
||||
FileDropEvent::Ended,
|
||||
]
|
||||
.map(PlatformInput::FileDrop))
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
//! Files dropped on the terminal type their paths, as macOS Terminal does.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use gpui::{Context, ExternalPaths};
|
||||
|
||||
use super::view::TerminalView;
|
||||
|
||||
/// What a shell reads specially in a path, which Ghostty escapes in one dropped.
|
||||
const SPECIAL: &str = "\\ ()[]{}<>\"'`!#$&;|*?";
|
||||
|
||||
/// The text `paths` type, each followed by a space: escaped for a Unix shell, or quoted when it holds a space, as Windows Terminal quotes it.
|
||||
pub fn typed(paths: &[PathBuf], windows: bool) -> anyhow::Result<String> {
|
||||
paths
|
||||
.iter()
|
||||
.map(|path| {
|
||||
let text = (path.to_str())
|
||||
.with_context(|| format!("{} is no UTF-8 a session can type", path.display()))?;
|
||||
anyhow::ensure!(
|
||||
!text.chars().any(char::is_control),
|
||||
"{text:?} holds a control character, which would act as typed"
|
||||
);
|
||||
Ok(if windows && text.contains(' ') {
|
||||
format!("\"{text}\" ")
|
||||
} else if windows {
|
||||
format!("{text} ")
|
||||
} else {
|
||||
let mut escaped = String::with_capacity(text.len() + 1);
|
||||
for ch in text.chars() {
|
||||
if SPECIAL.contains(ch) {
|
||||
escaped.push('\\');
|
||||
}
|
||||
escaped.push(ch);
|
||||
}
|
||||
escaped.push(' ');
|
||||
escaped
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl TerminalView {
|
||||
/// Types the dropped paths at the cursor as a paste; a path it cannot type stops the drop.
|
||||
pub(super) fn drop_paths(&mut self, dropped: &ExternalPaths, cx: &mut Context<Self>) {
|
||||
let paths = dropped.paths();
|
||||
match typed(paths, cfg!(windows)) {
|
||||
Ok(text) => {
|
||||
log::info!("terminal {}: {} paths dropped", self.pty.pid, paths.len());
|
||||
self.paste_text(&text, cx);
|
||||
}
|
||||
Err(error) => log::error!(
|
||||
"terminal {}: the drop types nothing: {error:#}",
|
||||
self.pty.pid
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn paths(all: &[&str]) -> Vec<PathBuf> {
|
||||
all.iter().map(PathBuf::from).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_unix_path_escapes_what_a_shell_reads_specially() {
|
||||
let typed = |all: &[&str]| typed(&paths(all), false).unwrap();
|
||||
assert_eq!(typed(&["/tmp/a.txt"]), "/tmp/a.txt ");
|
||||
assert_eq!(typed(&["/tmp/my file.png"]), "/tmp/my\\ file.png ");
|
||||
assert_eq!(typed(&["/a/it's (1)&$x"]), "/a/it\\'s\\ \\(1\\)\\&\\$x ");
|
||||
assert_eq!(typed(&["/a/中文 b"]), "/a/中文\\ b ");
|
||||
assert_eq!(
|
||||
typed(&["/a", "/b c"]),
|
||||
"/a /b\\ c ",
|
||||
"each followed by a space"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_windows_path_is_quoted_when_it_holds_a_space() {
|
||||
let typed = |all: &[&str]| typed(&paths(all), true).unwrap();
|
||||
assert_eq!(typed(&["C:\\a.txt"]), "C:\\a.txt ");
|
||||
assert_eq!(
|
||||
typed(&["C:\\My Files\\b.txt", "D:\\c"]),
|
||||
"\"C:\\My Files\\b.txt\" D:\\c "
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_path_with_a_control_character_types_nothing() {
|
||||
let error = typed(&paths(&["/ok", "/tmp/a\nls"]), false).unwrap_err();
|
||||
assert!(error.to_string().contains("control character"), "{error}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//! The terminal's font, cell and metrics.
|
||||
|
||||
use gpui::{
|
||||
Font, FontFeatures, FontId, FontStyle, FontWeight, Pixels, TextSystem, Window, px, size,
|
||||
};
|
||||
|
||||
use super::{ink::Ink, paint::Look};
|
||||
use crate::settings::Settings;
|
||||
|
||||
/// The bundled Nerd Font that draws icons any font lacks; gpui loads no font without an `m`.
|
||||
pub const ICONS: &str = "JetBrainsMono Nerd Font Mono";
|
||||
|
||||
/// The bundled code font, the default.
|
||||
pub const MONO: &str = "JetBrains Mono";
|
||||
|
||||
/// `font` as gpui loads it; gpui falls back to another font without a word, and loads none without an `m`.
|
||||
pub fn load_font(font: &Font, system: &TextSystem) -> anyhow::Result<FontId> {
|
||||
let id = system.resolve_font(font);
|
||||
let loaded = system.get_font_for_id(id);
|
||||
anyhow::ensure!(
|
||||
loaded.is_some_and(|loaded| loaded.family == font.family),
|
||||
"font_family is {:?}, which the terminal cannot load",
|
||||
font.family
|
||||
);
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Font, cell and metrics at `points` on this window.
|
||||
pub(super) fn look(
|
||||
settings: &Settings,
|
||||
points: f32,
|
||||
ink: &Ink,
|
||||
window: &Window,
|
||||
) -> anyhow::Result<Look> {
|
||||
let font = Font {
|
||||
family: settings.font_family.clone().into(),
|
||||
features: if settings.ligatures {
|
||||
FontFeatures::default()
|
||||
} else {
|
||||
FontFeatures::disable_ligatures()
|
||||
},
|
||||
fallbacks: None,
|
||||
weight: FontWeight::NORMAL,
|
||||
style: FontStyle::Normal,
|
||||
};
|
||||
let text = px(points);
|
||||
let system = window.text_system();
|
||||
let id = load_font(&font, system)?;
|
||||
let advance = system
|
||||
.advance(id, text, 'M')
|
||||
.map_err(|error| anyhow::anyhow!("{} cannot measure M: {error}", settings.font_family))?;
|
||||
let scale = window.scale_factor();
|
||||
let snap = |length: Pixels| px((f32::from(length) * scale).round() / scale);
|
||||
let cell = size(
|
||||
snap(advance.width).max(px(1.)),
|
||||
snap(text * settings.line_height),
|
||||
);
|
||||
let (ascent, descent) = (system.ascent(id, text), system.descent(id, text).abs());
|
||||
let icons = Font {
|
||||
family: ICONS.into(),
|
||||
features: FontFeatures::default(),
|
||||
..font.clone()
|
||||
};
|
||||
Ok(Look {
|
||||
font_id: id,
|
||||
icons,
|
||||
baseline: (cell.height - ascent - descent) / 2. + ascent,
|
||||
descent,
|
||||
rule: snap((text / 14.).max(px(1.))),
|
||||
size: text,
|
||||
cell,
|
||||
font,
|
||||
cursor: ink.cursor,
|
||||
cursor_text: ink.cursor_text,
|
||||
fg: ink.fg,
|
||||
bg: ink.bg,
|
||||
})
|
||||
}
|
||||
+4
-1
@@ -1,6 +1,8 @@
|
||||
mod boxes;
|
||||
mod click;
|
||||
mod drop;
|
||||
mod find;
|
||||
mod fonts;
|
||||
pub mod frame;
|
||||
mod herdr;
|
||||
pub mod ink;
|
||||
@@ -29,6 +31,7 @@ mod view;
|
||||
mod win32;
|
||||
pub mod zoom;
|
||||
|
||||
pub use fonts::{ICONS, MONO, load_font};
|
||||
pub use pty::{Events, Launch, Listener, Pty, PtyEvent, config, fit, spawn};
|
||||
pub use status::{Kind, Record, Records, State};
|
||||
pub use view::{BLINK, ICONS, MONO, TerminalEvent, TerminalView, load_font};
|
||||
pub use view::{BLINK, TerminalEvent, TerminalView};
|
||||
+6
-1
@@ -378,6 +378,11 @@ impl TerminalView {
|
||||
log::debug!("terminal {}: no text to paste", self.pty.pid);
|
||||
return;
|
||||
};
|
||||
self.paste_text(&text, cx);
|
||||
}
|
||||
|
||||
/// `text` to the program as a paste, bracketed when it asked.
|
||||
pub(super) fn paste_text(&mut self, text: &str, cx: &mut Context<Self>) {
|
||||
let bracketed = self
|
||||
.pty
|
||||
.term
|
||||
@@ -390,7 +395,7 @@ impl TerminalView {
|
||||
text.chars().count(),
|
||||
if bracketed { ", bracketed" } else { "" }
|
||||
);
|
||||
self.input(keys::paste(&text, bracketed), cx);
|
||||
self.input(keys::paste(text, bracketed), cx);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-73
@@ -12,20 +12,20 @@ use ely_gpui_component::{
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
Bounds, Context, EventEmitter, FocusHandle, Focusable, Font, FontFeatures, FontId, FontStyle,
|
||||
FontWeight, IntoElement, MouseButton, ParentElement, Pixels, Render, Styled, Subscription,
|
||||
Task, TextSystem, Window, div, prelude::*, px, size,
|
||||
Bounds, Context, EventEmitter, ExternalPaths, FocusHandle, Focusable, IntoElement, MouseButton,
|
||||
ParentElement, Pixels, Render, Styled, Subscription, Task, Window, div, prelude::*, px,
|
||||
};
|
||||
|
||||
use super::{
|
||||
click::Shell,
|
||||
find::Finder,
|
||||
fonts::look,
|
||||
frame::frame,
|
||||
ink::Ink,
|
||||
input::{self, Find, Held, NextPrompt, PreviousPrompt},
|
||||
marks::Mark,
|
||||
mouse::MouseMode,
|
||||
paint::{self, Look},
|
||||
paint,
|
||||
pointer::Pointer,
|
||||
pty::{Events, Pty, PtyEvent, config, fit},
|
||||
reports::Notices,
|
||||
@@ -33,16 +33,7 @@ use super::{
|
||||
status::{Records, State},
|
||||
zoom,
|
||||
};
|
||||
use crate::{
|
||||
settings::{Settings, SettingsStore},
|
||||
themes,
|
||||
};
|
||||
|
||||
/// The bundled Nerd Font that draws icons any font lacks; gpui loads no font without an `m`.
|
||||
pub const ICONS: &str = "JetBrainsMono Nerd Font Mono";
|
||||
|
||||
/// The bundled code font, the default.
|
||||
pub const MONO: &str = "JetBrains Mono";
|
||||
use crate::{settings::SettingsStore, themes};
|
||||
|
||||
/// How long the cursor shows, then hides, while it blinks.
|
||||
pub const BLINK: Duration = Duration::from_millis(600);
|
||||
@@ -349,65 +340,6 @@ impl TerminalView {
|
||||
}
|
||||
}
|
||||
|
||||
/// `font` as gpui loads it; gpui falls back to another font without a word, and loads none without an `m`.
|
||||
pub fn load_font(font: &Font, system: &TextSystem) -> anyhow::Result<FontId> {
|
||||
let id = system.resolve_font(font);
|
||||
let loaded = system.get_font_for_id(id);
|
||||
anyhow::ensure!(
|
||||
loaded.is_some_and(|loaded| loaded.family == font.family),
|
||||
"font_family is {:?}, which the terminal cannot load",
|
||||
font.family
|
||||
);
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Font, cell and metrics at `points` on this window.
|
||||
fn look(settings: &Settings, points: f32, ink: &Ink, window: &Window) -> anyhow::Result<Look> {
|
||||
let font = Font {
|
||||
family: settings.font_family.clone().into(),
|
||||
features: if settings.ligatures {
|
||||
FontFeatures::default()
|
||||
} else {
|
||||
FontFeatures::disable_ligatures()
|
||||
},
|
||||
fallbacks: None,
|
||||
weight: FontWeight::NORMAL,
|
||||
style: FontStyle::Normal,
|
||||
};
|
||||
let text = px(points);
|
||||
let system = window.text_system();
|
||||
let id = load_font(&font, system)?;
|
||||
let advance = system
|
||||
.advance(id, text, 'M')
|
||||
.map_err(|error| anyhow::anyhow!("{} cannot measure M: {error}", settings.font_family))?;
|
||||
let scale = window.scale_factor();
|
||||
let snap = |length: Pixels| px((f32::from(length) * scale).round() / scale);
|
||||
let cell = size(
|
||||
snap(advance.width).max(px(1.)),
|
||||
snap(text * settings.line_height),
|
||||
);
|
||||
let (ascent, descent) = (system.ascent(id, text), system.descent(id, text).abs());
|
||||
let icons = Font {
|
||||
family: ICONS.into(),
|
||||
features: FontFeatures::default(),
|
||||
..font.clone()
|
||||
};
|
||||
Ok(Look {
|
||||
font_id: id,
|
||||
icons,
|
||||
baseline: (cell.height - ascent - descent) / 2. + ascent,
|
||||
descent,
|
||||
rule: snap((text / 14.).max(px(1.))),
|
||||
size: text,
|
||||
cell,
|
||||
font,
|
||||
cursor: ink.cursor,
|
||||
cursor_text: ink.cursor_text,
|
||||
fg: ink.fg,
|
||||
bg: ink.bg,
|
||||
})
|
||||
}
|
||||
|
||||
impl Render for TerminalView {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = SettingsStore::get(cx).clone();
|
||||
@@ -454,6 +386,7 @@ impl Render for TerminalView {
|
||||
.on_any_mouse_down(cx.listener(Self::mouse_down))
|
||||
.on_mouse_move(cx.listener(Self::mouse_move))
|
||||
.on_scroll_wheel(cx.listener(Self::wheel))
|
||||
.on_drop(cx.listener(|view, paths: &ExternalPaths, _, cx| view.drop_paths(paths, cx)))
|
||||
.size_full()
|
||||
.px(px(settings.padding_x))
|
||||
.py(px(settings.padding_y))
|
||||
|
||||
@@ -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 |
|
||||
| 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 |
|
||||
| Drop: files dropped on the terminal type their paths at the cursor, escaped for the shell, quoted on Windows, each followed by a space | T66 | `term::drop` tests: a Unix path escapes what a shell reads specially, spaces, quotes, parentheses, `&` and `$` among them, CJK kept, each path followed by a space; a Windows path is quoted when it holds a space; a path with a control character types nothing; on macOS `shot` dropped `/tmp/my file.png`, `/Users/x/it's (1).txt` and `/a/中文 b` on zsh after a pointer move, and the log read `3 paths dropped` and `pastes 53 characters, bracketed`; without the move, after typing, gpui passed the drop by, as AGENTS records | 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; round 4 found the newest typed line taken by its place: after `printf '\e[H'` fish drew its prompt at the top over older lines, and a click on its typed line did nothing; the newest line is now the one of the highest number, and a press goes no lower than its last row or the cursor's, so that click gave `abcXdef`, the reviewer's scene sent `ESC[<0;6;1M`, and a click on the older text below moved nothing, where fish had moved its cursor | Pass |
|
||||
| A bell makes no sound; the row marks it | T10, T12 | T12 review | Pass |
|
||||
|
||||
|
||||
Reference in new issue
Block a user