From 37220a72d46f0a43778452b6436e32969e38a09a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 15 Oct 2026 10:15:53 -0400 Subject: [PATCH] T22: Edit's Copy, Paste and Select All are Ely's text input actions, which Ely a2cb531 exports and the terminal handles too, so the menu reaches find and every field; Select All counts a space under a combining mark as a character; comments cut to one short line; the Ely pin moves to a2cb531; AGENTS.md says gpui draws a shot window only while macOS shows it --- AGENTS.md | 4 ++-- Cargo.lock | 2 +- Cargo.toml | 2 +- examples/shot/capture.rs | 8 ++++---- examples/shot/main.rs | 2 +- examples/shot/typing.rs | 8 ++++---- src/menus.rs | 12 +++++------- src/term/input.rs | 5 +++-- src/term/pointer.rs | 35 +++++++++++++++++++++++++---------- src/term/view.rs | 9 ++++++--- src/term/zoom.rs | 8 ++++---- 11 files changed, 56 insertions(+), 39 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0703989..f53f4f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,7 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does; ## Commands - `cargo run` opens the app. `RUST_LOG=elyterminal=debug cargo run` logs more; `trace` logs each paint. -- `cargo run --example shot -- [--keys "SPECS"] [args…]` runs a program in the main window, plays SPECS on it, and writes a PNG of the window (macOS; no permission needed for the app's own window), naming the AppKit views under its content: gpui's blur is a `BlurredView`, and a capture of one window shows its alpha, not the blur behind it; it logs the menu bar, each item with its key. It shows its window without activating the app, so gpui keeps drawing it and keystrokes or clicks meant for other apps stay there. Run it with `HOME` pointing at a temp folder to try settings without touching your own. SPECS, space-separated: +- `cargo run --example shot -- [--keys "SPECS"] [args…]` runs a program in the main window, plays SPECS on it, and writes a PNG of the window (macOS; no permission needed for the app's own window), naming the AppKit views under its content: gpui's blur is a `BlurredView`, and a capture of one window shows its alpha, not the blur behind it; it logs the menu bar, each item with its key. It shows its window without activating the app, so keystrokes or clicks meant for other apps stay there; gpui draws a window only while macOS shows it, so one on another Space or under a full-screen app keeps its last frame. Run it with `HOME` pointing at a temp folder to try settings without touching your own. SPECS, space-separated: - keys as real AppKit events: `a`, `ctrl-c`, `lopt-f` and `ropt-f` (left and right Option), `cmd-v`, `repeat-up` (AppKit's repeat flag); - input method calls: `mark:é` composes, `commit:é` types; - `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; @@ -74,4 +74,4 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does; - 2026-10-14: the main window lets the desktop through while Background opacity is under 1, blurred or clear as Background blur says, through gpui's `Blurred` (macOS's NSVisualEffectView, Windows' acrylic) or `Transparent`; at 1 it is `Opaque`. The workspace sets it as the settings change. Ely's `ColumnShell::glass`, added to Ely for this, grounds the columns in the palette's glass, which the opacity sets, so the title bar and every column let the desktop through alike; the palette's `bg` stays solid, so a dialog, the Git diff among them, hides what lies under it. The terminal paints no ground over the glass, or the two would stack; a program's own background (OSC 11) paints over it. Ely's own themes pass their palette too, Ely's colors with the glass set. The settings window stays opaque. - 2026-10-15: on Wayland the window asks for `ext_background_effect_v1` beside gpui's `org_kde_kwin_blur`. A guest connection on gpui's own display binds it, takes the effect for the window's surface once, and blurs the window's pane: the surface less Ely's resize margin on each free edge of a client-drawn window, worked out each frame and sent only when it changes; it goes and comes back with Blurred and lands at the surface's next commit, which every frame makes. The guest connection reads the display only while the window opens, before gpui's loop runs, and is never dropped: a drop destroys its proxies through the display, which gpui may close first. The log names what the compositor offers, so which way blur goes: `ext_background_effect_v1`, gpui's `org_kde_kwin_blur`, or none, the desktop clear. Its test runs a `wayland-server` compositor on a socket pair. - 2026-10-15: on a client-drawn window, so on Linux, Ely's `ResizeBorder` paints its pane solid and shadowed under the content, which kept the desktop from showing at all. `ResizeBorder::glass`, added to Ely for this, leaves the pane to the content while the window is see-through: no ground, nor the shadow gpui draws under the box, which would tint it. So the blur covers the pane alone and the resize margin stays clear. On X11 the window names its pane in `_KDE_NET_WM_BLUR_BEHIND_REGION` (CARDINAL, 32: x, y, width, height in the window's pixels), as Ghostty names its own, and drops the property when Blurred goes; KWin reads it, other compositors pass it by. It goes over a connection of ELYTerminal's own to `$DISPLAY`, which gpui opens too: a property belongs to the window, whoever sets it. gpui's own `org_kde_kwin_blur` on KDE's Wayland still covers the whole surface, margin included, beyond ELYTerminal's reach. -- 2026-10-15: the macOS menu bar is the PRD's: ELYTerminal (Settings…, Quit), Shell (New Session, Close Session), Edit (Copy, Paste, Select All, Find), View (Toggle Sessions, Toggle Right Panel, Bigger, Smaller, Reset Size), Window (Minimize, Zoom, then the windows macOS lists). An item shows the key bound to its action, so the menus are set after the bindings; Copy, Paste and Select All go through AppKit's `copy:`, `paste:` and `selectAll:`, as gpui's `OsAction` maps them. The new keys are Ghostty's: Cmd-Q, Cmd-A, Cmd-= and Cmd-+, Cmd-minus, Cmd-0, Cmd-M. Off macOS Quit and Select All take Ctrl-Shift, since Ctrl with a letter types a control byte; the font's size takes plain Ctrl with =, +, minus and 0, which type none, as in Ghostty, Alacritty and Windows Terminal; Minimize has no key there. Bigger and Smaller move every session's font a point, inside the Size range, and Reset Size goes back to the setting; nothing saves the change, as in Ghostty. Select All runs from the first character in the history to the last on the screen, the blanks around them left out, as Ghostty's; copy on select takes it, and a blank grid selects nothing. +- 2026-10-15: the macOS menu bar is the PRD's: ELYTerminal (Settings…, Quit), Shell (New Session, Close Session), Edit (Copy, Paste, Select All, Find), View (Toggle Sessions, Toggle Right Panel, Bigger, Smaller, Reset Size), Window (Minimize, Zoom, then the windows macOS lists). An item shows the key bound to its action, so the menus are set after the bindings; Copy, Paste and Select All go through AppKit's `copy:`, `paste:` and `selectAll:`, as gpui's `OsAction` maps them. They are Ely's text input actions, which `forms` exports for this, and the terminal handles them too: gpui sends a menu's action to what holds focus and enables the item only where a handler waits, so one action serves the terminal, find and every field. The new keys are Ghostty's: Cmd-Q, Cmd-A, Cmd-= and Cmd-+, Cmd-minus, Cmd-0, Cmd-M. Off macOS Quit and Select All take Ctrl-Shift, since Ctrl with a letter types a control byte; the font's size takes plain Ctrl with =, +, minus and 0, which type none, as in Ghostty, Alacritty and Windows Terminal; Minimize has no key there. Bigger and Smaller move every session's font a point, inside the Size range, and Reset Size goes back to the setting; nothing saves the change, as in Ghostty. Select All runs from the first character in the history to the last on the screen, the blanks around them left out, as Ghostty's, though a space under a combining mark shows a character and counts as one; copy on select takes it, and a blank grid selects nothing. diff --git a/Cargo.lock b/Cargo.lock index 6137b42..1b708dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1701,7 +1701,7 @@ checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" [[package]] name = "ely-gpui-component" version = "0.1.1" -source = "git+https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git?rev=5dc4dd1805550b8697130dce2c8eda903ce33f54#5dc4dd1805550b8697130dce2c8eda903ce33f54" +source = "git+https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git?rev=a2cb531e06e15f9e15d5b44a83f82beddb4888f9#a2cb531e06e15f9e15d5b44a83f82beddb4888f9" dependencies = [ "alacritty_terminal", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 1d70702..d7d754f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ publish = false alacritty_terminal = { version = "=0.26.0", default-features = false } anyhow = "1" dirs = "7" -ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git", rev = "5dc4dd1805550b8697130dce2c8eda903ce33f54" } +ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git", rev = "a2cb531e06e15f9e15d5b44a83f82beddb4888f9" } env_logger = "0.11" futures = "0.3" gethostname = "1.1.0" diff --git a/examples/shot/capture.rs b/examples/shot/capture.rs index 0b36729..53acbef 100644 --- a/examples/shot/capture.rs +++ b/examples/shot/capture.rs @@ -17,7 +17,7 @@ pub fn number(window: &gpui::Window) -> anyhow::Result { u32::try_from(number).context("window number out of range") } -/// Shows the window and makes it the app's key window, which AppKit sends keys to, without activating the app: gpui draws it, real input stays put. +/// Shows the window as key, the app left inactive. pub fn front(number: u32) -> anyhow::Result<()> { unsafe { let app: *mut Object = msg_send![class!(NSApplication), sharedApplication]; @@ -29,7 +29,7 @@ pub fn front(number: u32) -> anyhow::Result<()> { Ok(()) } -/// The class of each view under the window's content, as AppKit names it; gpui's blur is a `BlurredView`. +/// AppKit's class of each view under the content. pub fn views(number: u32) -> anyhow::Result> { unsafe { let app: *mut Object = msg_send![class!(NSApplication), sharedApplication]; @@ -50,7 +50,7 @@ pub fn views(number: u32) -> anyhow::Result> { } } -/// The menu bar's items as AppKit holds them: menu, item, then the key and its modifier mask. +/// Each menu item with its key and modifier mask. pub fn menus() -> Vec { let text = |string: *mut Object| -> String { let bytes: *const std::ffi::c_char = unsafe { msg_send![string, UTF8String] }; @@ -80,7 +80,7 @@ pub fn menus() -> Vec { lines } -/// Writes the window `number` as a PNG; the app's own window needs no permission. +/// Writes window `number` as a PNG. pub fn snapshot(number: u32, path: &Path) -> anyhow::Result<()> { use core_graphics::{ geometry::{CGPoint, CGRect, CGSize}, diff --git a/examples/shot/main.rs b/examples/shot/main.rs index d833fcc..2dca284 100644 --- a/examples/shot/main.rs +++ b/examples/shot/main.rs @@ -1,4 +1,4 @@ -//! Runs a program in the main window, plays keys and mouse gestures on it, photographs it and names its AppKit views; `to:settings` and `to:main` aim what follows. +//! Plays SPECS on the app's windows and photographs one. #[cfg(target_os = "macos")] fn main() { diff --git a/examples/shot/typing.rs b/examples/shot/typing.rs index d47be50..da66e1a 100644 --- a/examples/shot/typing.rs +++ b/examples/shot/typing.rs @@ -59,7 +59,7 @@ fn string(text: &str) -> *mut Object { unsafe { msg_send![class!(NSString), stringWithUTF8String: text.as_ptr()] } } -/// `mark` composes `text` as an input method does; `commit` types it. +/// An input method's `mark` or `commit` of `text`. fn compose(call: &str, text: &str, number: u32) -> anyhow::Result<()> { let none = Range { location: i64::MAX as u64, @@ -114,7 +114,7 @@ fn steps(gesture: &str, points: &[CGPoint]) -> anyhow::Result anyhow::Result<(CGEventType, CGMouseButton, &'static str)> { Ok(match step { "ldown" => ( @@ -153,7 +153,7 @@ fn kind(step: &str) -> anyhow::Result<(CGEventType, CGMouseButton, &'static str) }) } -/// Hands gpui's view a mouse gesture as AppKit events, even while the window is inactive. +/// Hands gpui's view a gesture as AppKit events. fn mouse(name: &str, flags: u64, number: u32) -> anyhow::Result<()> { let mut fields = name.split('@'); let gesture = fields.next().context("a gesture")?; @@ -214,7 +214,7 @@ fn mouse(name: &str, flags: u64, number: u32) -> anyhow::Result<()> { Ok(()) } -/// Posts `spec`, a key, an input method call or a mouse gesture, on window `number`. +/// Posts one spec on window `number`. pub fn post(spec: &str, number: u32) -> anyhow::Result<()> { if let Some((call, text)) = spec.split_once(':') { return compose(call, text, number); diff --git a/src/menus.rs b/src/menus.rs index 78f6bc6..88d5ae3 100644 --- a/src/menus.rs +++ b/src/menus.rs @@ -1,11 +1,9 @@ +use ely_gpui_component::forms::{Copy, Paste, SelectAll}; use gpui::{App, InteractiveElement, KeyBinding, Menu, MenuItem, OsAction, actions}; use crate::{ settings::window::OpenSettings, - term::{ - input::{Copy, Find, Paste, SelectAll}, - zoom, - }, + term::{input::Find, zoom}, workspace::{CloseSession, NewSession, TogglePanel, ToggleSessions}, }; @@ -14,7 +12,7 @@ actions!( [Quit, Bigger, Smaller, ResetSize, Minimize, Zoom] ); -/// The menu bar the PRD lists; macOS draws it and names the first menu after the app. +/// The PRD's menu bar. pub fn menus() -> Vec { let menu = |name: &str, items| Menu { name: name.to_string().into(), @@ -69,7 +67,7 @@ pub fn menus() -> Vec { ] } -/// Ghostty's keys for Quit and the font's size, Cmd on macOS and Ctrl elsewhere, Cmd-M to minimize; what they do; then the menus, which show the keys bound by now. +/// Binds app commands before building their menus. pub fn init(cx: &mut App) { let (quit, modifier) = if cfg!(target_os = "macos") { ("cmd-q", "cmd") @@ -97,7 +95,7 @@ pub fn init(cx: &mut App) { cx.set_menus(menus()); } -/// Minimize and Zoom on a window's root: only a window answers them. +/// Minimize and Zoom, which only a window answers. pub fn window_actions(root: E) -> E { root.on_action(|_: &Minimize, window, _| { log::info!("window: minimized"); diff --git a/src/term/input.rs b/src/term/input.rs index 0185026..9e65802 100644 --- a/src/term/input.rs +++ b/src/term/input.rs @@ -1,6 +1,7 @@ use std::ops::Range; use alacritty_terminal::{grid::Scroll, term::TermMode}; +use ely_gpui_component::forms::{Copy, Paste, SelectAll}; use gpui::{ App, Bounds, Context, EntityInputHandler, KeyBinding, KeyDownEvent, KeyUpEvent, Keystroke, NoAction, Pixels, Point, UTF16Selection, Window, actions, @@ -11,7 +12,7 @@ use super::{ view::TerminalView, }; -actions!(elyterminal, [Copy, Paste, Find, FindNewer, SelectAll]); +actions!(elyterminal, [Find, FindNewer]); /// The key context a terminal's root sets. pub const CONTEXT: &str = "Terminal"; @@ -19,7 +20,7 @@ pub const CONTEXT: &str = "Terminal"; /// The key context of a terminal's find box. pub const FIND: &str = "TerminalFind"; -/// Copy, paste, select all and find; Ely's focus keys go to the terminal's listeners instead. +/// The terminal's keys; Ely's focus keys reach its listeners. pub fn bind_keys(cx: &mut App) { let (copy, paste, all, find) = if cfg!(target_os = "macos") { ("cmd-c", "cmd-v", "cmd-a", "cmd-f") diff --git a/src/term/pointer.rs b/src/term/pointer.rs index 7a9a2b5..79cd9cc 100644 --- a/src/term/pointer.rs +++ b/src/term/pointer.rs @@ -298,7 +298,7 @@ impl TerminalView { (!text.is_empty()).then_some(text) } - /// Selects the history and the screen, blanks around them left out; copy on select takes the text. + /// Selects all text; copy on select takes it. pub(super) fn select_all(&mut self, cx: &mut Context) { { let mut term = self.pty.term.lock(); @@ -367,13 +367,17 @@ impl TerminalView { } } -/// Ghostty's select all: the first cell holding more than a space or tab, through history and screen, to the last. +/// Ghostty's select all: first to last non-blank cell. fn everything(term: &Term) -> Option { let grid = term.grid(); let cells = (grid.topmost_line().0..=grid.bottommost_line().0).flat_map(|line| { (0..grid.columns()).map(move |column| Cell::new(Line(line), Column(column))) }); - let inked = |cell: &Cell| !matches!(grid[*cell].c, ' ' | '\t'); + let inked = |point: &Cell| { + let cell = &grid[*point]; + let marked = cell.zerowidth().is_some_and(|marks| !marks.is_empty()); + marked || !matches!(cell.c, ' ' | '\t') + }; let first = cells.clone().find(inked)?; let last = cells.rev().find(inked)?; let mut all = Selection::new(SelectionType::Simple, first, Side::Left); @@ -391,14 +395,25 @@ mod tests { use super::*; + /// What select all copies after `bytes`. + fn all(bytes: &[u8]) -> Option { + let mut term = Term::new(Config::default(), &TermSize::new(10, 3), VoidListener); + let mut parser: Processor = Processor::new(); + parser.advance(&mut term, bytes); + term.selection = everything(&term); + term.selection_to_string() + } + #[test] fn everything_runs_from_the_first_character_in_the_history_to_the_last() { - let mut term = Term::new(Config::default(), &TermSize::new(10, 3), VoidListener); - assert!(everything(&term).is_none(), "a blank grid selects nothing"); - let mut parser: Processor = Processor::new(); - parser.advance(&mut term, b"\r\n one\r\n\r\ntwo six\t\r\n\r\n"); - term.selection = everything(&term); - let text = term.selection_to_string().expect("a selection"); - assert_eq!(text, "one\n\ntwo six"); + assert_eq!(all(b""), None, "a blank grid selects nothing"); + let text = all(b"\r\n one\r\n\r\ntwo six\t\r\n\r\n"); + assert_eq!(text.as_deref(), Some("one\n\ntwo six")); + let mark = all(" \u{301}".as_bytes()); + assert_eq!(mark.as_deref(), Some(" \u{301}"), "a mark on a space"); + let last = all("text \u{301}".as_bytes()); + assert_eq!(last.as_deref(), Some("text \u{301}"), "a mark ends it"); + let link = all(b"\x1b]8;;http://a\x07 \x1b]8;;\x07text"); + assert_eq!(link.as_deref(), Some("text"), "a linked space is blank"); } } diff --git a/src/term/view.rs b/src/term/view.rs index 0de22f7..7fe4dd7 100644 --- a/src/term/view.rs +++ b/src/term/view.rs @@ -6,7 +6,10 @@ use alacritty_terminal::{ term::{ClipboardType, TermMode}, vte::ansi::CursorShape, }; -use ely_gpui_component::theme::ActiveTheme; +use ely_gpui_component::{ + forms::{Copy, Paste, SelectAll}, + theme::ActiveTheme, +}; use futures::StreamExt; use gpui::{ Bounds, Context, EventEmitter, FocusHandle, Focusable, Font, FontFeatures, FontId, FontStyle, @@ -18,7 +21,7 @@ use super::{ find::Finder, frame::frame, ink::Ink, - input::{self, Copy, Find, Paste, SelectAll}, + input::{self, Find}, mouse::MouseMode, paint::{self, Look}, pointer::Pointer, @@ -269,7 +272,7 @@ pub fn load_font(font: &Font, system: &TextSystem) -> anyhow::Result { Ok(id) } -/// The font, cell and metrics the settings give at `points` and this window's scale. +/// Font, cell and metrics at `points` on this window. fn look(settings: &Settings, points: f32, ink: &Ink, window: &Window) -> anyhow::Result { let font = Font { family: settings.font_family.clone().into(), diff --git a/src/term/zoom.rs b/src/term/zoom.rs index 06d5c58..7e51af3 100644 --- a/src/term/zoom.rs +++ b/src/term/zoom.rs @@ -2,18 +2,18 @@ use gpui::{App, Global}; use crate::settings::{FONT_SIZE, SettingsStore}; -/// Points the terminal's font stands above the Size setting, below it when negative; Bigger and Smaller move it, nothing saves it. +/// Unsaved font-size offset. #[derive(Default)] pub struct Enlarged(f32); impl Global for Enlarged {} -/// The size the terminal draws its font at: the setting, moved, inside the range. +/// The font size the terminal draws. pub fn size(cx: &App) -> f32 { shown(SettingsStore::get(cx).font_size, cx.global::().0) } -/// Moves the font `by` points, or back to the setting when `by` is none. +/// Moves the font `by` points; none resets it. pub fn step(by: Option, cx: &mut App) { let setting = SettingsStore::get(cx).font_size; let enlarged = by.map_or(0.0, |by| moved(setting, cx.global::().0, by)); @@ -28,7 +28,7 @@ fn shown(setting: f32, enlarged: f32) -> f32 { (setting + enlarged).clamp(*FONT_SIZE.start(), *FONT_SIZE.end()) } -/// What `enlarged` becomes once the font moves `by` points from where it shows. +/// The offset after moving `by` from the shown size. fn moved(setting: f32, enlarged: f32, by: f32) -> f32 { shown(shown(setting, enlarged) + by, 0.0) - setting }