diff --git a/AGENTS.md b/AGENTS.md index f2b194c..469bf7d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,10 @@ 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 "a ctrl-c lopt-f repeat-up mark:é commit:é"] [args…]` runs a program in the main window, types the keys as real AppKit events (`lopt`/`ropt` are the left and right Option keys, `repeat-` sets AppKit's repeat flag, `mark:` and `commit:` hand text to the window's input client as an input method does), and writes a PNG of the window (macOS; no permission needed for the app's own window). It never activates its window, so it takes no keystrokes or clicks meant for other apps. Run it with `HOME` pointing at a temp folder to try settings without touching your own. +- `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). 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: + - 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; + - 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. ## Layout @@ -20,7 +23,7 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does; - `src/main.rs`: start. `src/lib.rs`: the modules, public so tests and examples reach them. `src/app.rs`: fonts, settings, integration, then the main window. - `src/settings`: the model, its ranges, and `SettingsStore`, the global that loads, checks and saves `settings.json`. - `src/themes`: built-in terminal themes, the chrome palette each maps to, and `apply`, which dresses the app for the system's appearance. -- `src/term`: the terminal engine. `pty` starts a program through alacritty's event loop; `tap` wraps the pty so its reader scans OSC 7 and mode 9 before the parser. `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. `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 and repeat flag of AppKit's current event, keeps held keys repeating, and hands Command key-ups to the focused view. `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. +- `src/term`: the terminal engine. `pty` starts a program through alacritty's event loop; `tap` wraps the pty so its reader scans OSC 7 and the mouse modes before the parser. `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. `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 and repeat flag of AppKit's current event, keeps held keys repeating, and hands Command key-ups to the focused view. `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. - `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. - `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). @@ -49,7 +52,7 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does; - 2026-10-14: an input method's commit goes to the program as UTF-8 under every kitty flag, as kitty (`keys.c`, `GLFW_IME_COMMIT_TEXT`) and Ghostty (test `kitty: composed text with report all`) send it. - 2026-10-14: what gpui hides from the kitty protocol: a key alone on a modifier sends nothing (gpui reports no key event for it), and shifted punctuation reports the character it types as its key, since gpui clears Shift for it. - 2026-10-14: on macOS, which Option key is down and whether a key repeats come from AppKit's current event: gpui rebuilds keys AppKit routes through its commands as fresh presses. As in Ghostty, `ApplePressAndHoldEnabled` is registered off, so held keys repeat instead of opening the accent menu, and a local event monitor hands Command key-ups to the focused view, since AppKit never routes them to the window. -- 2026-10-14: mouse reports follow Ghostty's rules: X10 tracking sends presses alone without modifiers, SGR names the released button, motion inside one cell sends nothing, Shift takes the mouse back for selecting. X10 tracking is mode 9, which vte drops; the tap follows `CSI ? 9 h` and `l`, and 1000, 1002, 1003 and a reset end it. +- 2026-10-14: mouse reports follow Ghostty's rules: X10 tracking sends presses alone without modifiers, SGR names the released button, motion inside one cell sends nothing, Shift takes the mouse back for selecting. The tap follows modes 9, 1000, 1002, 1003, 1005 and 1006 as xterm holds them: one tracking and one format at a time, the last set wins, any reset ends it, and RIS clears both. alacritty drops 9 and keeps each mode apart, so its mouse bits go unread. Motion reports the first held button: left, right, middle. - 2026-10-14: a wheel notch scrolls 3 lines, as Ghostty's discrete wheel; a trackpad scrolls its pixels. The wheel reports to a program that tracks the mouse, sends arrows on the alternate screen under mode 1007, and scrolls history otherwise. - 2026-10-14: Cmd-C and Cmd-V copy and paste on macOS, Ctrl-Shift-C and Ctrl-Shift-V elsewhere; Cmd-press, Ctrl-press off macOS, opens an OSC 8 target or a URL Ely's `links` finds in the wrapped line. Copies drop the newline alacritty ends a whole line with, as Ghostty's do. Bracketed pastes lose ESC and Ctrl-C, as alacritty's do; plain pastes end lines with CR. Typing and pasting clear the selection. - 2026-10-14: a key's release goes out only when the terminal saw its press, so a key a binding takes sends no release, as in kitty. diff --git a/examples/shot.rs b/examples/shot.rs index 3ae7749..b7d241d 100644 --- a/examples/shot.rs +++ b/examples/shot.rs @@ -1,4 +1,4 @@ -//! `cargo run --example shot -- [--keys "a ctrl-c lopt-f repeat-up mark:é commit:é shift-click@40,20"] [args…]`: runs a program in the main window, types the keys and mouse gestures as AppKit events, then photographs the window. +//! Runs a program in the main window, plays keys and mouse gestures on it, and photographs it. #[cfg(target_os = "macos")] fn main() { @@ -31,11 +31,12 @@ fn main() { app::start(cx).expect("the app starts"); let window = app::open(launch, cx).expect("the window opens"); cx.spawn(async move |cx| { - cx.background_executor().timer(Duration::from_secs(1)).await; let number = window .update(cx, |_, window, _| capture::number(window)) .expect("the window lives") .expect("a window number"); + capture::front(number).expect("the window on screen"); + cx.background_executor().timer(Duration::from_secs(1)).await; for spec in &keys { typing::post(spec, number).expect("a key event"); cx.background_executor() @@ -60,7 +61,7 @@ mod capture { use std::path::Path; use anyhow::{Context as _, bail}; - use objc::{msg_send, runtime::Object, sel, sel_impl}; + use objc::{class, msg_send, runtime::Object, sel, sel_impl}; use raw_window_handle::{HasWindowHandle, RawWindowHandle}; /// A gpui window's number, as CoreGraphics knows it. @@ -76,6 +77,17 @@ mod capture { u32::try_from(number).context("window number out of range") } + /// Shows the window without activating the app: gpui draws it, real input stays put. + pub fn front(number: u32) -> anyhow::Result<()> { + unsafe { + let app: *mut Object = msg_send![class!(NSApplication), sharedApplication]; + let window: *mut Object = msg_send![app, windowWithWindowNumber: i64::from(number)]; + anyhow::ensure!(!window.is_null(), "no window {number}"); + let _: () = msg_send![window, orderFrontRegardless]; + } + Ok(()) + } + /// Writes the window `number` as a PNG; the app's own window needs no permission. pub fn snapshot(number: u32, path: &Path) -> anyhow::Result<()> { use core_graphics::{ @@ -117,14 +129,14 @@ mod capture { mod typing { use anyhow::Context as _; use core_graphics::{ - event::{CGEvent, CGEventFlags, ScrollEventUnit}, + event::{CGEvent, CGEventFlags, CGEventType, CGMouseButton, EventField, ScrollEventUnit}, event_source::{CGEventSource, CGEventSourceStateID}, geometry::{CGPoint, CGRect}, }; use foreign_types::ForeignType; use objc::{ class, msg_send, - runtime::{NO, Object, YES}, + runtime::{NO, Object, Sel, YES}, sel, sel_impl, }; @@ -196,7 +208,68 @@ mod typing { fn CGEventSetLocation(event: *mut core_graphics::sys::CGEvent, location: CGPoint); } - /// `click@x,y` (`click2`, `click3`, `rclick`), `drag@x,y@x,y`, `move@x,y`, `scrollup@x,y@lines` or `scrolldown`, at points from the content's top-left; AppKit routes no window-less scroll, so it goes to the view. + /// A gesture's steps: each one's name, point and click count. + fn steps(gesture: &str, points: &[CGPoint]) -> anyhow::Result> { + let step = |name: &str, at: CGPoint, clicks| (name.to_string(), at, clicks); + let (at, to) = (points[0], points.get(1).copied()); + Ok(match gesture { + "click" | "click2" | "click3" => (1..=gesture[5..].parse().unwrap_or(1)) + .flat_map(|clicks| [step("ldown", at, clicks), step("lup", at, clicks)]) + .collect(), + "rclick" => vec![step("rdown", at, 1), step("rup", at, 1)], + "mclick" => vec![step("mdown", at, 1), step("mup", at, 1)], + "drag" => { + let to = to.context("drag needs a second point")?; + vec![ + step("ldown", at, 1), + step("ldrag", to, 1), + step("lup", to, 1), + ] + } + name => vec![step(name, at, 1)], + }) + } + + /// A step's event type, button and the selector gpui's view takes it by. + fn kind(step: &str) -> anyhow::Result<(CGEventType, CGMouseButton, &'static str)> { + Ok(match step { + "ldown" => ( + CGEventType::LeftMouseDown, + CGMouseButton::Left, + "mouseDown:", + ), + "lup" => (CGEventType::LeftMouseUp, CGMouseButton::Left, "mouseUp:"), + "ldrag" => ( + CGEventType::LeftMouseDragged, + CGMouseButton::Left, + "mouseDragged:", + ), + "rdown" => ( + CGEventType::RightMouseDown, + CGMouseButton::Right, + "rightMouseDown:", + ), + "rup" => ( + CGEventType::RightMouseUp, + CGMouseButton::Right, + "rightMouseUp:", + ), + "mdown" => ( + CGEventType::OtherMouseDown, + CGMouseButton::Center, + "otherMouseDown:", + ), + "mup" => ( + CGEventType::OtherMouseUp, + CGMouseButton::Center, + "otherMouseUp:", + ), + "move" => (CGEventType::MouseMoved, CGMouseButton::Left, "mouseMoved:"), + other => anyhow::bail!("no mouse gesture {other}"), + }) + } + + /// Hands gpui's view a mouse gesture as AppKit events, even while the window is inactive. fn mouse(name: &str, flags: u64, number: u32) -> anyhow::Result<()> { let mut fields = name.split('@'); let gesture = fields.next().context("a gesture")?; @@ -205,84 +278,61 @@ mod typing { .map(str::parse) .collect::>() .with_context(|| format!("numbers in {name}"))?; - let needed = match gesture { - "drag" => 4, - "scrollup" | "scrolldown" => 3, - _ => 2, - }; - anyhow::ensure!(numbers.len() == needed, "{name} needs {needed} numbers"); + anyhow::ensure!(numbers.len() >= 2, "{name} needs a point"); unsafe { let app: *mut Object = msg_send![class!(NSApplication), sharedApplication]; let window: *mut Object = msg_send![app, windowWithWindowNumber: i64::from(number)]; anyhow::ensure!(!window.is_null(), "no window {number}"); + let view: *mut Object = msg_send![window, firstResponder]; let content: *mut Object = msg_send![window, contentView]; - let height = { - let frame: CGRect = msg_send![content, frame]; - frame.size.height + let frame: CGRect = msg_send![content, frame]; + let height = frame.size.height; + let points: Vec = numbers + .chunks_exact(2) + .map(|pair| CGPoint::new(pair[0], height - pair[1])) + .collect(); + let screens: *mut Object = msg_send![class!(NSScreen), screens]; + let main: *mut Object = msg_send![screens, objectAtIndex: 0u64]; + let screen: CGRect = msg_send![main, frame]; + let source = || { + CGEventSource::new(CGEventSourceStateID::Private) + .map_err(|_| anyhow::anyhow!("no event source")) }; - let at = |index: usize| CGPoint::new(numbers[index], height - numbers[index + 1]); - let post = |kind: u64, point: CGPoint, clicks: i64| -> anyhow::Result<()> { - let event: *mut Object = msg_send![ - class!(NSEvent), - mouseEventWithType: kind - location: point - modifierFlags: flags - timestamp: 0.0f64 - windowNumber: i64::from(number) - context: std::ptr::null_mut::() - eventNumber: 0i64 - clickCount: clicks - pressure: 1.0f32 - ]; - anyhow::ensure!(!event.is_null(), "AppKit refused the mouse event {name}"); - let _: () = msg_send![app, postEvent: event atStart: NO]; + let deliver = |event: CGEvent, at: CGPoint, selector: &str| -> anyhow::Result<()> { + CGEventSetLocation( + event.as_ptr(), + CGPoint::new(at.x, screen.size.height - at.y), + ); + event.set_flags(CGEventFlags::from_bits_truncate(flags)); + let native: *mut Object = + msg_send![class!(NSEvent), eventWithCGEvent: event.as_ptr()]; + anyhow::ensure!(!native.is_null(), "AppKit refused {name}"); + let selector = Sel::register(selector); + let _: *mut Object = msg_send![view, performSelector: selector withObject: native]; Ok(()) }; - match gesture { - "click" | "click2" | "click3" => { - for clicks in 1..=gesture[5..].parse().unwrap_or(1) { - post(1, at(0), clicks)?; - post(2, at(0), clicks)?; - } + if let Some(up) = gesture.strip_prefix("scroll") { + let lines = *numbers.get(2).context("a scroll needs lines")? as i32; + let lines = if up == "up" { lines } else { -lines }; + let event = + CGEvent::new_scroll_event(source()?, ScrollEventUnit::LINE, 1, lines, 0, 0) + .map_err(|_| anyhow::anyhow!("no scroll event"))?; + deliver(event, points[0], "scrollWheel:")?; + } else { + for (step, at, clicks) in steps(gesture, &points)? { + let (kind, button, selector) = kind(&step)?; + let event = CGEvent::new_mouse_event(source()?, kind, at, button) + .map_err(|_| anyhow::anyhow!("no mouse event"))?; + event.set_integer_value_field(EventField::MOUSE_EVENT_CLICK_STATE, clicks); + deliver(event, at, selector)?; } - "rclick" => { - post(3, at(0), 1)?; - post(4, at(0), 1)?; - } - "drag" => { - post(1, at(0), 1)?; - post(6, at(2), 1)?; - post(2, at(2), 1)?; - } - "move" => post(5, at(0), 0)?, - "scrollup" | "scrolldown" => { - let lines = numbers[2] as i32 * if gesture == "scrollup" { 1 } else { -1 }; - let screens: *mut Object = msg_send![class!(NSScreen), screens]; - let main: *mut Object = msg_send![screens, objectAtIndex: 0u64]; - let screen: CGRect = msg_send![main, frame]; - let point = at(0); - let source = CGEventSource::new(CGEventSourceStateID::Private) - .map_err(|_| anyhow::anyhow!("no event source"))?; - let event = - CGEvent::new_scroll_event(source, ScrollEventUnit::LINE, 1, lines, 0, 0) - .map_err(|_| anyhow::anyhow!("no scroll event"))?; - let place = CGPoint::new(point.x, screen.size.height - point.y); - CGEventSetLocation(event.as_ptr(), place); - event.set_flags(CGEventFlags::from_bits_truncate(flags)); - let native: *mut Object = - msg_send![class!(NSEvent), eventWithCGEvent: event.as_ptr()]; - anyhow::ensure!(!native.is_null(), "AppKit refused the scroll {name}"); - let view: *mut Object = msg_send![window, firstResponder]; - let _: () = msg_send![view, scrollWheel: native]; - } - other => anyhow::bail!("no mouse gesture {other}"), } } log::info!("shot: mouse {name}"); Ok(()) } - /// Posts `spec`, such as `ctrl-c`, `lopt-f`, `repeat-up` or `shift-drag@0,0@40,20`, as AppKit events on window `number`. + /// Posts `spec`, a key, an input method call or a mouse gesture, 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/term/input.rs b/src/term/input.rs index 3b4a09b..caf07ab 100644 --- a/src/term/input.rs +++ b/src/term/input.rs @@ -38,7 +38,9 @@ impl TerminalView { pub(super) fn input(&mut self, bytes: Vec, cx: &mut Context) { let mut term = self.pty.term.lock(); term.scroll_display(Scroll::Bottom); - term.selection = None; + if term.selection.take().is_some() { + log::debug!("terminal {}: input clears the selection", self.pty.pid); + } drop(term); self.pty.write(bytes); cx.notify(); diff --git a/src/term/keys.rs b/src/term/keys.rs index edc84b6..2e23789 100644 --- a/src/term/keys.rs +++ b/src/term/keys.rs @@ -51,7 +51,7 @@ pub fn encode(key: &Key, app_cursor: bool, kitty: Kitty) -> Option> { } } -/// Pasted text as the program gets it; bracketed text loses ESC and Ctrl-C, as alacritty's does. +/// Pasted text for the program; bracketed text loses ESC and Ctrl-C, as in alacritty. pub fn paste(text: &str, bracketed: bool) -> Vec { if bracketed { let text = text.replace(['\x1b', '\x03'], ""); diff --git a/src/term/links.rs b/src/term/links.rs index 436f71a..8e5df75 100644 --- a/src/term/links.rs +++ b/src/term/links.rs @@ -4,7 +4,7 @@ use alacritty_terminal::{ }; use ely_gpui_component::terminal::{Target, links}; -/// The link at `point`: the program's OSC 8 target, else a URL in its line, wrapped rows joined. +/// The OSC 8 target at `point`, else a URL in its wrapped line. pub fn link_at(term: &Term, mut point: Point) -> Option { let grid = term.grid(); if grid[point].flags.contains(Flags::WIDE_CHAR_SPACER) { @@ -23,6 +23,7 @@ pub fn link_at(term: &Term, mut point: Point) -> Option { let spacer = Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER; if !grid[cell].flags.intersects(spacer) { text.push(grid[cell].c); + text.extend(grid[cell].zerowidth().into_iter().flatten()); } if cell == end { break; @@ -70,6 +71,12 @@ mod tests { assert_eq!(link(text, 0, 11), None); } + #[test] + fn a_url_keeps_its_combining_marks() { + let text = "https://e.test/cafe\u{301}/x"; + assert_eq!(link(text, 1, 1).as_deref(), Some(text)); + } + #[test] fn a_url_runs_across_wrapped_rows_and_wide_cells() { let text = "\u{4e2d} https://ely.dev/abc"; diff --git a/src/term/mouse.rs b/src/term/mouse.rs index 4e11c11..7f42b54 100644 --- a/src/term/mouse.rs +++ b/src/term/mouse.rs @@ -1,5 +1,3 @@ -use alacritty_terminal::term::TermMode; - /// A button as reports name it; the wheel's two turns count as buttons. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Button { @@ -32,8 +30,9 @@ pub struct Report { } /// The mouse events a program asked for. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum Tracking { + #[default] Off, /// Mode 9: presses alone, no modifiers. X10, @@ -45,30 +44,51 @@ pub enum Tracking { Any, } -impl Tracking { - /// The tracking `mode` sets; `x10` is mode 9, which the tap follows. - pub fn new(mode: TermMode, x10: bool) -> Self { - if x10 { - Self::X10 - } else if mode.contains(TermMode::MOUSE_MOTION) { - Self::Any - } else if mode.contains(TermMode::MOUSE_DRAG) { - Self::Button - } else if mode.contains(TermMode::MOUSE_REPORT_CLICK) { - Self::Normal - } else { - Self::Off - } +/// The bytes reports go in: xterm's default, mode 1005 or mode 1006. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum Format { + #[default] + X10, + Utf8, + Sgr, +} + +/// How a program asked for the mouse. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct MouseMode { + pub tracking: Tracking, + pub format: Format, +} + +impl MouseMode { + /// Follows `CSI ? mode h` or `l`; as in xterm, a reset ends the tracking or format. + pub fn apply(&mut self, mode: u32, set: bool) { + let tracking = match mode { + 9 => Tracking::X10, + 1000 => Tracking::Normal, + 1002 => Tracking::Button, + 1003 => Tracking::Any, + 1005 | 1006 => { + self.format = match (set, mode) { + (false, _) => Format::X10, + (true, 1005) => Format::Utf8, + (true, _) => Format::Sgr, + }; + return; + } + _ => return, + }; + self.tracking = if set { tracking } else { Tracking::Off }; } } -/// The bytes `report` sends; none when the program did not ask for it or its format cannot hold the cell. -pub fn encode(report: &Report, tracking: Tracking, mode: TermMode) -> Option> { +/// The bytes `report` sends; none when unasked for or past what the format holds. +pub fn encode(report: &Report, mode: MouseMode) -> Option> { let clicked = matches!( report.button, Some(Button::Left | Button::Middle | Button::Right) ); - let wanted = match tracking { + let wanted = match mode.tracking { Tracking::Off => false, Tracking::X10 => report.gesture == Gesture::Press && clicked, Tracking::Normal => report.gesture != Gesture::Motion, @@ -78,7 +98,7 @@ pub fn encode(report: &Report, tracking: Tracking, mode: TermMode) -> Option 3, Some(_) if report.gesture == Gesture::Release && !sgr => 3, @@ -88,7 +108,7 @@ pub fn encode(report: &Report, tracking: Tracking, mode: TermMode) -> Option 64, Some(Button::WheelDown) => 65, }; - if tracking != Tracking::X10 { + if mode.tracking != Tracking::X10 { code += 4 * u32::from(report.shift) + 8 * u32::from(report.alt) + 16 * u32::from(report.ctrl); } @@ -104,7 +124,7 @@ pub fn encode(report: &Report, tracking: Tracking, mode: TermMode) -> Option most || row > most { log::debug!("mouse: cell {column},{row} is past what the format holds"); @@ -139,102 +159,104 @@ mod tests { } } - fn sent(report: Report, tracking: Tracking, mode: TermMode) -> Option> { - encode(&report, tracking, mode) + fn mode(tracking: Tracking, format: Format) -> MouseMode { + MouseMode { tracking, format } } const LEFT: Option