// SPDX-License-Identifier: GPL-2.0-only // // rs-mrxvt — a modernized, distro-agnostic mrxvt-inspired terminal emulator. // // Copyright (C) 2024 rs-mrxvt contributors // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along // with this program; if not, see . //! Mouse event types and SGR mouse encoding. //! //! The classic mrxvt supported several mouse modes (X10, X11 normal, X11 //! SGR-1006). This module defines the backend-agnostic mouse event type and //! the SGR encoder that translates mouse events into escape sequences for //! the child process. //! //! ## Mouse modes //! //! Programs request mouse reporting via DECSET escape sequences: //! - `?9h` — X10 (click only, no modifiers, no release) //! - `?1000h` — X11 normal (press/release + motion-with-button) //! - `?1002h` — X11 motion (all motion events, even with no button) //! - `?1003h` — all motion (no button needed) //! - `?1006h` — SGR-1006 encoding (extends the above with bigger coords //! and explicit press/release markers) //! //! The terminal keeps a `MouseMode` bitfield; the renderer translates raw //! mouse events into [`MouseEvent`]s and asks the encoder whether to send //! them to the child. //! //! ## Selection //! //! When mouse reporting is OFF, mouse events are interpreted locally as //! text selection: click-drag selects, release copies to clipboard. This //! module exposes a [`Selection`] state machine that renderers can drive. use std::fmt; /// Bitflags for active mouse modes. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct MouseMode { /// X10 (button press only). pub x10: bool, /// X11 normal (press + release + motion-with-button). pub x11: bool, /// Motion reporting (even with no button held). pub motion: bool, /// SGR-1006 encoding. pub sgr: bool, } impl MouseMode { /// Is any mouse reporting active? pub fn any_reporting(self) -> bool { self.x10 || self.x11 || self.motion } /// Should a button-press event be reported? pub fn reports_press(self) -> bool { self.x10 || self.x11 || self.motion } /// Should a button-release event be reported? pub fn reports_release(self) -> bool { self.x11 || self.motion } /// Should a motion event be reported? pub fn reports_motion(self, button_held: bool) -> bool { self.motion || (self.x11 && button_held) } } /// A mouse button. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MouseButton { Left, Middle, Right, /// Wheel up (one notch). WheelUp, /// Wheel down (one notch). WheelDown, /// No button (used for motion events with no button held). None, } /// A mouse event. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct MouseEvent { pub button: MouseButton, /// Cell column (0-indexed). pub col: u32, /// Cell row (0-indexed). pub row: u32, pub mods: MouseMods, pub kind: MouseEventKind, } /// Modifier flags on a mouse event. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct MouseMods { pub shift: bool, pub ctrl: bool, pub alt: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MouseEventKind { Press, Release, Motion, } /// Encode a mouse event using SGR-1006 format. /// /// Returns `None` if the event shouldn't be reported (e.g. release in X10 mode). /// /// SGR-1006 format: /// - Press: `ESC [ < button ; col ; row M` /// - Release: `ESC [ < button ; col ; row m` /// /// Where `button` is the button code + modifier bits (lower 3 bits = button, /// bit 2 = shift, bit 3 = meta, bit 4 = ctrl, bit 5 = wheel, bit 6 = motion /// flag — actually for SGR we just encode button+mods, motion is a separate /// M vs m signal). pub fn encode_sgr(ev: MouseEvent, mode: MouseMode) -> Option> { let code = button_code(ev.button, ev.mods); let suffix = match ev.kind { MouseEventKind::Press => { if !mode.reports_press() { return None; } 'M' } MouseEventKind::Release => { if !mode.reports_release() { return None; } 'm' } MouseEventKind::Motion => { if !mode.reports_motion(ev.button != MouseButton::None) { return None; } // SGR-1006 uses M for motion-with-button, m for motion-without. // Convention: motion events always use M; the button field // encodes which button (or 35 = no button) is held. 'M' } }; // SGR is 1-indexed. let col = ev.col + 1; let row = ev.row + 1; let s = format!("\x1b[<{code};{col};{row}{suffix}"); Some(s.into_bytes()) } /// Encode a mouse event using legacy X11 format (for programs that don't /// support SGR-1006). /// /// Returns `None` if the event shouldn't be reported. /// /// Legacy format: `ESC [ M ` /// where each byte is the value + 32 (to keep it in the printable range). /// Coordinates are clamped to 1..227 (bytes 33..255). pub fn encode_x11(ev: MouseEvent, mode: MouseMode) -> Option> { let _should = match ev.kind { MouseEventKind::Press => mode.reports_press(), MouseEventKind::Release => mode.reports_release(), MouseEventKind::Motion => mode.reports_motion(ev.button != MouseButton::None), }; if !_should { return None; } let mut code = button_code(ev.button, ev.mods); if ev.kind == MouseEventKind::Motion { code |= 32; // motion flag } let b = (code.min(255 - 32) + 32) as u8; // Coords: 1-indexed, clamped to 1..223 (bytes 33..255 after +32). // Clamp to 223 so that 223 + 32 = 255 = u8::MAX (no overflow). let col_clamped = (ev.col.saturating_add(1)).clamp(1, 223); let row_clamped = (ev.row.saturating_add(1)).clamp(1, 223); let c = (col_clamped + 32) as u8; let r = (row_clamped + 32) as u8; Some(vec![0x1b, b'[', b'M', b, c, r]) } /// Compute the button code (lower 3 bits + modifier bits). fn button_code(button: MouseButton, mods: MouseMods) -> u32 { let base = match button { MouseButton::Left => 0, MouseButton::Middle => 1, MouseButton::Right => 2, MouseButton::WheelUp => 64, MouseButton::WheelDown => 65, MouseButton::None => 3, }; let mod_bits: [(bool, u32); 3] = [(mods.shift, 4), (mods.alt, 8), (mods.ctrl, 16)]; base | mod_bits.iter().filter(|(f, _)| *f).map(|(_, b)| b).fold(0, |a, b| a | b) } // ─── Selection state machine ───────────────────────────────────────────────── /// A text selection. Used by renderers for click-drag-to-select. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct Selection { /// Start point (cell coords). `None` = no active selection. pub start: Option<(u32, u32)>, /// End point (cell coords). `None` = single-click. pub end: Option<(u32, u32)>, } impl Selection { pub fn new() -> Self { Self::default() } /// Begin a selection at the given cell. pub fn begin(&mut self, col: u32, row: u32) { self.start = Some((col, row)); self.end = Some((col, row)); } /// Extend the selection to the given cell. pub fn extend(&mut self, col: u32, row: u32) { if self.start.is_some() { self.end = Some((col, row)); } } /// Clear the selection. pub fn clear(&mut self) { self.start = None; self.end = None; } /// Is there an active selection? pub fn is_active(&self) -> bool { self.start.is_some() && self.end.is_some() } /// Iterate over the selected cells in row-major order. pub fn cells(&self) -> Vec<(u32, u32)> { let Some((sx, sy)) = self.start else { return Vec::new(); }; let Some((ex, ey)) = self.end else { return Vec::new(); }; // Normalize: top-left to bottom-right. let (x1, y1) = (sx.min(ex), sy.min(ey)); let (x2, y2) = (sx.max(ex), sy.max(ey)); (y1..=y2).flat_map(|y| (x1..=x2).map(move |x| (x, y))).collect() } /// True if the selection spans more than one cell. pub fn is_multi_cell(&self) -> bool { if let (Some(s), Some(e)) = (self.start, self.end) { s != e } else { false } } } impl fmt::Display for MouseButton { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?}", self) } } #[cfg(test)] mod tests { use super::*; #[test] fn mouse_mode_no_reporting_by_default() { let m = MouseMode::default(); assert!(!m.any_reporting()); assert!(!m.reports_press()); assert!(!m.reports_release()); assert!(!m.reports_motion(false)); } #[test] fn x11_mode_reports_press_and_release() { let m = MouseMode { x11: true, ..Default::default() }; assert!(m.reports_press()); assert!(m.reports_release()); assert!(!m.reports_motion(false)); assert!(m.reports_motion(true)); } #[test] fn motion_mode_reports_all() { let m = MouseMode { motion: true, ..Default::default() }; assert!(m.reports_motion(false)); assert!(m.reports_motion(true)); } #[test] fn x10_only_reports_press() { let m = MouseMode { x10: true, ..Default::default() }; assert!(m.reports_press()); assert!(!m.reports_release()); assert!(!m.reports_motion(true)); } #[test] fn sgr_press_encoding() { let ev = MouseEvent { button: MouseButton::Left, col: 5, row: 10, mods: MouseMods::default(), kind: MouseEventKind::Press, }; let mode = MouseMode { x11: true, sgr: true, ..Default::default() }; let bytes = encode_sgr(ev, mode).unwrap(); let s = String::from_utf8(bytes).unwrap(); // SGR is 1-indexed: col 5 → 6, row 10 → 11. assert_eq!(s, "\x1b[<0;6;11M"); } #[test] fn sgr_release_encoding() { let ev = MouseEvent { button: MouseButton::Right, col: 0, row: 0, mods: MouseMods::default(), kind: MouseEventKind::Release, }; let mode = MouseMode { x11: true, sgr: true, ..Default::default() }; let bytes = encode_sgr(ev, mode).unwrap(); let s = String::from_utf8(bytes).unwrap(); assert_eq!(s, "\x1b[<2;1;1m"); } #[test] fn sgr_with_modifiers() { let ev = MouseEvent { button: MouseButton::Left, col: 0, row: 0, mods: MouseMods { shift: true, ctrl: true, alt: true }, kind: MouseEventKind::Press, }; let mode = MouseMode { x11: true, sgr: true, ..Default::default() }; let bytes = encode_sgr(ev, mode).unwrap(); let s = String::from_utf8(bytes).unwrap(); // shift=4, alt=8, ctrl=16, left=0 → 28 assert_eq!(s, "\x1b[<28;1;1M"); } #[test] fn sgr_wheel_events() { let up = MouseEvent { button: MouseButton::WheelUp, col: 3, row: 4, mods: MouseMods::default(), kind: MouseEventKind::Press, }; let mode = MouseMode { x11: true, sgr: true, ..Default::default() }; let s = String::from_utf8(encode_sgr(up, mode).unwrap()).unwrap(); // WheelUp = 64 assert_eq!(s, "\x1b[<64;4;5M"); } #[test] fn sgr_skipped_when_mode_off() { let ev = MouseEvent { button: MouseButton::Left, col: 0, row: 0, mods: MouseMods::default(), kind: MouseEventKind::Press, }; let mode = MouseMode::default(); assert!(encode_sgr(ev, mode).is_none()); } #[test] fn x11_legacy_encoding() { let ev = MouseEvent { button: MouseButton::Left, col: 0, row: 0, mods: MouseMods::default(), kind: MouseEventKind::Press, }; let mode = MouseMode { x11: true, ..Default::default() }; let bytes = encode_x11(ev, mode).unwrap(); // ESC [ M <32> <33> <33> assert_eq!(bytes, vec![0x1b, b'[', b'M', 32, 33, 33]); } #[test] fn x11_legacy_clamps_coords() { let ev = MouseEvent { button: MouseButton::Left, col: 500, row: 500, mods: MouseMods::default(), kind: MouseEventKind::Press, }; let mode = MouseMode { x11: true, ..Default::default() }; let bytes = encode_x11(ev, mode).unwrap(); // Coords clamped to 223 before +32, so byte = 255 (no u8 overflow). assert_eq!(bytes.len(), 6); assert_eq!(bytes[4], 255); // col byte assert_eq!(bytes[5], 255); // row byte } #[test] fn selection_begin_and_extend() { let mut s = Selection::new(); assert!(!s.is_active()); s.begin(0, 0); assert!(s.is_active()); s.extend(5, 2); assert!(s.is_multi_cell()); } #[test] fn selection_cells_row_major() { let mut s = Selection::new(); s.begin(0, 0); s.extend(2, 1); let cells = s.cells(); // 3 cols × 2 rows = 6 cells. assert_eq!(cells.len(), 6); assert!(cells.contains(&(0, 0))); assert!(cells.contains(&(2, 1))); } #[test] fn selection_normalizes_swapped_points() { let mut s = Selection::new(); s.begin(5, 5); s.extend(1, 1); let cells = s.cells(); // Should still cover the same rectangle regardless of direction. assert_eq!(cells.len(), 5 * 5); assert!(cells.contains(&(1, 1))); assert!(cells.contains(&(5, 5))); } #[test] fn selection_clear() { let mut s = Selection::new(); s.begin(0, 0); s.clear(); assert!(!s.is_active()); } #[test] fn selection_single_cell_not_multi() { let mut s = Selection::new(); s.begin(3, 3); assert!(!s.is_multi_cell()); } }