aboutsummaryrefslogtreecommitdiff
path: root/src/pty_callbacks.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/pty_callbacks.rs')
-rw-r--r--src/pty_callbacks.rs450
1 files changed, 0 insertions, 450 deletions
diff --git a/src/pty_callbacks.rs b/src/pty_callbacks.rs
deleted file mode 100644
index 53483e51..00000000
--- a/src/pty_callbacks.rs
+++ /dev/null
@@ -1,450 +0,0 @@
-//! Terminal query responses.
-//!
-//! Programs like fzf, htop, and vim send escape sequences asking "what terminal am I?"
-//! and "where's the cursor?". A real terminal responds immediately. We need to do the same.
-//!
-//! There are ~15 query types that real programs use. We handle the common ones.
-//! The vt100 parser handles all OUTPUT sequences (formatting, cursor, etc.) — those
-//! don't need responses. Only QUERY sequences need us to reply.
-
-use std::sync::{
- atomic::{AtomicBool, AtomicU16, Ordering},
- Arc, Mutex,
-};
-
-/// Collects response bytes that need to be written back to the PTY.
-/// Also tracks whether the child has requested the Kitty keyboard protocol.
-#[derive(Clone)]
-pub struct PtyResponses {
- pending: Arc<Mutex<Vec<u8>>>,
- /// Stack of kitty keyboard enhancement flags pushed by child programs.
- /// Each push adds a flags value; pop removes the top. This correctly handles
- /// nested programs (e.g. fish pushes mode 1, neovim pushes mode 3, neovim
- /// pops → fish's mode 1 is restored).
- kitty_stack: Arc<Mutex<Vec<u16>>>,
- /// Derived from kitty_stack: true when stack is non-empty.
- /// Kept for quick feature checks.
- pub kitty_keyboard: Arc<AtomicBool>,
- /// Exact active kitty keyboard flags from the top of the stack.
- pub kitty_keyboard_flags: Arc<AtomicU16>,
- /// Tracks DECSET 1007 (alternate scroll mode).
- /// Default on, matching Ghostty/xterm-style behavior for fullscreen apps.
- pub mouse_alternate_scroll: Arc<AtomicBool>,
-}
-
-impl Default for PtyResponses {
- fn default() -> Self {
- Self {
- pending: Arc::default(),
- kitty_stack: Arc::default(),
- kitty_keyboard: Arc::new(AtomicBool::new(false)),
- kitty_keyboard_flags: Arc::new(AtomicU16::new(0)),
- mouse_alternate_scroll: Arc::new(AtomicBool::new(true)),
- }
- }
-}
-
-#[cfg_attr(feature = "ghostty-vt", allow(dead_code))]
-impl PtyResponses {
- pub fn new() -> Self {
- Self::default()
- }
-
- /// Take any pending response bytes (clears the buffer).
- pub fn take(&self) -> Vec<u8> {
- let mut pending = self.pending.lock().unwrap();
- std::mem::take(&mut *pending)
- }
-
- fn push(&self, bytes: &[u8]) {
- self.pending.lock().unwrap().extend_from_slice(bytes);
- }
-
- fn has_private_mode(params: &[&[u16]], mode: u16) -> bool {
- params
- .iter()
- .any(|param| param.len() == 1 && param[0] == mode)
- }
-}
-
-impl vt100::Callbacks for PtyResponses {
- fn unhandled_csi(
- &mut self,
- screen: &mut vt100::Screen,
- i1: Option<u8>,
- _i2: Option<u8>,
- params: &[&[u16]],
- c: char,
- ) {
- let param0 = params.first().and_then(|p| p.first()).copied().unwrap_or(0);
-
- match (i1, c) {
- // === Device Attributes ===
-
- // DA1: \e[c or \e[0c → "what terminal are you?"
- // Respond as VT220 with ANSI color
- (None, 'c') if param0 == 0 => {
- self.push(b"\x1b[?62;22c");
- }
-
- // DA2: \e[>c or \e[>0c → "secondary device attributes"
- (Some(b'>'), 'c') => {
- // Type 0 (VT100), firmware version 0, ROM version 0
- self.push(b"\x1b[>0;0;0c");
- }
-
- // === Cursor / Status Reports ===
-
- // DSR: \e[Nn where N selects the report type
- (None, 'n') => match param0 {
- // CPR: \e[6n → cursor position report
- 5 => {
- // Device status: "OK"
- self.push(b"\x1b[0n");
- }
- 6 => {
- let (row, col) = screen.cursor_position();
- let response = format!("\x1b[{};{}R", row + 1, col + 1);
- self.push(response.as_bytes());
- }
- _ => {}
- },
-
- // DECXCPR: \e[?6n → extended cursor position report (with page)
- (Some(b'?'), 'n') if param0 == 6 => {
- let (row, col) = screen.cursor_position();
- let response = format!("\x1b[?{};{}R", row + 1, col + 1);
- self.push(response.as_bytes());
- }
-
- // === Mode Queries (DECRQM) ===
-
- // DECRQM: \e[?Np → "is DEC private mode N set?"
- // Response: \e[?N;Ps$y where Ps = 1 (set), 2 (reset), 0 (unknown)
- (Some(b'?'), 'p') => {
- let state = match param0 {
- 1007 => {
- if self.mouse_alternate_scroll.load(Ordering::Relaxed) {
- 1
- } else {
- 2
- }
- }
- _ => 2,
- };
- let response = format!("\x1b[?{param0};{state}$y");
- self.push(response.as_bytes());
- }
-
- // ANSI DECRQM: \e[Np → "is ANSI mode N set?"
- (None, 'p') => {
- let response = format!("\x1b[{param0};2$y");
- self.push(response.as_bytes());
- }
-
- // === Keyboard Protocol ===
-
- // DECSET/DECRST 1007: alternate scroll mode.
- (Some(b'?'), 'h') if Self::has_private_mode(params, 1007) => {
- self.mouse_alternate_scroll.store(true, Ordering::Relaxed);
- }
- (Some(b'?'), 'l') if Self::has_private_mode(params, 1007) => {
- self.mouse_alternate_scroll.store(false, Ordering::Relaxed);
- }
-
- // Kitty keyboard query: \e[?u → "what keyboard flags are active?"
- (Some(b'?'), 'u') => {
- let stack = self.kitty_stack.lock().unwrap();
- let flags = stack.last().copied().unwrap_or(0);
- self.push(format!("\x1b[?{flags}u").as_bytes());
- }
-
- // Kitty keyboard push: \e[>Nu → child wants Kitty key encoding
- (Some(b'>'), 'u') => {
- let mut stack = self.kitty_stack.lock().unwrap();
- stack.push(param0);
- self.kitty_keyboard.store(true, Ordering::Relaxed);
- self.kitty_keyboard_flags.store(param0, Ordering::Relaxed);
- }
-
- // Kitty keyboard pop: \e[<Nu → child reverts to previous mode
- // N = number of entries to pop (default 1)
- (Some(b'<'), 'u') => {
- let mut stack = self.kitty_stack.lock().unwrap();
- let count = (param0 as usize).max(1);
- for _ in 0..count {
- if stack.pop().is_none() {
- break;
- }
- }
- let flags = stack.last().copied().unwrap_or(0);
- self.kitty_keyboard
- .store(!stack.is_empty(), Ordering::Relaxed);
- self.kitty_keyboard_flags.store(flags, Ordering::Relaxed);
- }
-
- // === Terminal Identification ===
-
- // XTVERSION: \e[>q → "what terminal version?"
- (Some(b'>'), 'q') => {
- // Respond in DCS format: \eP>|herdr 0.1\e\\
- self.push(b"\x1bP>|herdr 0.1\x1b\\");
- }
-
- _ => {}
- }
- }
-
- fn unhandled_osc(&mut self, _screen: &mut vt100::Screen, params: &[&[u8]]) {
- let Some(cmd) = params.first() else { return };
-
- match *cmd {
- // OSC 10 ; ? ST → query foreground color
- b"10" => {
- if params.get(1) == Some(&&b"?"[..]) {
- // Respond with a default light foreground
- self.push(b"\x1b]10;rgb:cccc/cccc/cccc\x1b\\");
- }
- }
- // OSC 11 ; ? ST → query background color
- b"11" => {
- if params.get(1) == Some(&&b"?"[..]) {
- // Respond with a default dark background
- self.push(b"\x1b]11;rgb:1e1e/1e1e/2e2e\x1b\\");
- }
- }
- // OSC 12 ; ? ST → query cursor color
- b"12" => {
- if params.get(1) == Some(&&b"?"[..]) {
- self.push(b"\x1b]12;rgb:cccc/cccc/cccc\x1b\\");
- }
- }
- _ => {}
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- fn make_parser(responses: PtyResponses) -> vt100::Parser<PtyResponses> {
- vt100::Parser::new_with_callbacks(24, 80, 0, responses)
- }
-
- #[test]
- fn responds_to_da1() {
- let r = PtyResponses::new();
- let mut p = make_parser(r.clone());
- p.process(b"\x1b[c");
- assert_eq!(r.take(), b"\x1b[?62;22c");
- }
-
- #[test]
- fn responds_to_da1_explicit_zero() {
- let r = PtyResponses::new();
- let mut p = make_parser(r.clone());
- p.process(b"\x1b[0c");
- assert_eq!(r.take(), b"\x1b[?62;22c");
- }
-
- #[test]
- fn responds_to_da2() {
- let r = PtyResponses::new();
- let mut p = make_parser(r.clone());
- p.process(b"\x1b[>c");
- assert_eq!(r.take(), b"\x1b[>0;0;0c");
- }
-
- #[test]
- fn responds_to_cpr() {
- let r = PtyResponses::new();
- let mut p = make_parser(r.clone());
- p.process(b"\x1b[5;10H"); // move cursor to row 5, col 10
- p.process(b"\x1b[6n");
- assert_eq!(r.take(), b"\x1b[5;10R");
- }
-
- #[test]
- fn responds_to_dsr_status() {
- let r = PtyResponses::new();
- let mut p = make_parser(r.clone());
- p.process(b"\x1b[5n"); // device status report
- assert_eq!(r.take(), b"\x1b[0n"); // "OK"
- }
-
- #[test]
- fn responds_to_extended_cpr() {
- let r = PtyResponses::new();
- let mut p = make_parser(r.clone());
- p.process(b"\x1b[3;7H"); // move cursor
- p.process(b"\x1b[?6n"); // extended CPR
- assert_eq!(r.take(), b"\x1b[?3;7R");
- }
-
- #[test]
- fn responds_to_decrqm_private() {
- let r = PtyResponses::new();
- let mut p = make_parser(r.clone());
- p.process(b"\x1b[?25p"); // query: is cursor visible (mode 25)?
- assert_eq!(r.take(), b"\x1b[?25;2$y"); // "reset" (2)
- }
-
- #[test]
- fn alternate_scroll_defaults_on_and_reports_set() {
- let r = PtyResponses::new();
- let mut p = make_parser(r.clone());
-
- assert!(r.mouse_alternate_scroll.load(Ordering::Relaxed));
- p.process(b"\x1b[?1007p");
- assert_eq!(r.take(), b"\x1b[?1007;1$y");
- }
-
- #[test]
- fn decset_decrst_1007_updates_alternate_scroll_mode() {
- let r = PtyResponses::new();
- let mut p = make_parser(r.clone());
-
- p.process(b"\x1b[?1007l");
- assert!(!r.mouse_alternate_scroll.load(Ordering::Relaxed));
- p.process(b"\x1b[?1007p");
- assert_eq!(r.take(), b"\x1b[?1007;2$y");
-
- p.process(b"\x1b[?1007h");
- assert!(r.mouse_alternate_scroll.load(Ordering::Relaxed));
- p.process(b"\x1b[?1007p");
- assert_eq!(r.take(), b"\x1b[?1007;1$y");
- }
-
- #[test]
- fn responds_to_kitty_keyboard_query() {
- let r = PtyResponses::new();
- let mut p = make_parser(r.clone());
- p.process(b"\x1b[?u");
- assert_eq!(r.take(), b"\x1b[?0u");
- }
-
- #[test]
- fn kitty_push_pop_stack() {
- let r = PtyResponses::new();
- let mut p = make_parser(r.clone());
-
- // Initially off
- assert!(!r.kitty_keyboard.load(Ordering::Relaxed));
- assert_eq!(r.kitty_keyboard_flags.load(Ordering::Relaxed), 0);
-
- // Fish pushes mode 1
- p.process(b"\x1b[>1u");
- assert!(r.kitty_keyboard.load(Ordering::Relaxed));
- assert_eq!(r.kitty_keyboard_flags.load(Ordering::Relaxed), 1);
- p.process(b"\x1b[?u");
- assert_eq!(r.take(), b"\x1b[?1u");
-
- // Neovim pushes mode 3
- p.process(b"\x1b[>3u");
- assert!(r.kitty_keyboard.load(Ordering::Relaxed));
- assert_eq!(r.kitty_keyboard_flags.load(Ordering::Relaxed), 3);
- p.process(b"\x1b[?u");
- assert_eq!(r.take(), b"\x1b[?3u");
-
- // Neovim pops → fish's mode 1 is restored
- p.process(b"\x1b[<u");
- assert!(r.kitty_keyboard.load(Ordering::Relaxed));
- assert_eq!(r.kitty_keyboard_flags.load(Ordering::Relaxed), 1);
- p.process(b"\x1b[?u");
- assert_eq!(r.take(), b"\x1b[?1u");
-
- // Fish pops → back to legacy
- p.process(b"\x1b[<u");
- assert!(!r.kitty_keyboard.load(Ordering::Relaxed));
- assert_eq!(r.kitty_keyboard_flags.load(Ordering::Relaxed), 0);
- p.process(b"\x1b[?u");
- assert_eq!(r.take(), b"\x1b[?0u");
- }
-
- #[test]
- fn kitty_pop_on_empty_is_harmless() {
- let r = PtyResponses::new();
- let mut p = make_parser(r.clone());
-
- // Pop with nothing on stack — should not crash
- p.process(b"\x1b[<u");
- assert!(!r.kitty_keyboard.load(Ordering::Relaxed));
- assert_eq!(r.kitty_keyboard_flags.load(Ordering::Relaxed), 0);
- }
-
- #[test]
- fn kitty_pop_count() {
- let r = PtyResponses::new();
- let mut p = make_parser(r.clone());
-
- // Push three times
- p.process(b"\x1b[>1u");
- p.process(b"\x1b[>3u");
- p.process(b"\x1b[>5u");
- assert!(r.kitty_keyboard.load(Ordering::Relaxed));
- assert_eq!(r.kitty_keyboard_flags.load(Ordering::Relaxed), 5);
-
- // Pop 2 at once
- p.process(b"\x1b[<2u");
- assert!(r.kitty_keyboard.load(Ordering::Relaxed));
- assert_eq!(r.kitty_keyboard_flags.load(Ordering::Relaxed), 1);
- p.process(b"\x1b[?u");
- assert_eq!(r.take(), b"\x1b[?1u"); // only first push remains
-
- // Pop last one
- p.process(b"\x1b[<u");
- assert!(!r.kitty_keyboard.load(Ordering::Relaxed));
- assert_eq!(r.kitty_keyboard_flags.load(Ordering::Relaxed), 0);
- }
-
- #[test]
- fn responds_to_xtversion() {
- let r = PtyResponses::new();
- let mut p = make_parser(r.clone());
- p.process(b"\x1b[>q");
- assert_eq!(r.take(), b"\x1bP>|herdr 0.1\x1b\\");
- }
-
- #[test]
- fn responds_to_osc_fg_color_query() {
- let r = PtyResponses::new();
- let mut p = make_parser(r.clone());
- p.process(b"\x1b]10;?\x1b\\");
- assert_eq!(r.take(), b"\x1b]10;rgb:cccc/cccc/cccc\x1b\\");
- }
-
- #[test]
- fn responds_to_osc_bg_color_query() {
- let r = PtyResponses::new();
- let mut p = make_parser(r.clone());
- p.process(b"\x1b]11;?\x1b\\");
- assert_eq!(r.take(), b"\x1b]11;rgb:1e1e/1e1e/2e2e\x1b\\");
- }
-
- #[test]
- fn responds_to_osc_cursor_color_query() {
- let r = PtyResponses::new();
- let mut p = make_parser(r.clone());
- p.process(b"\x1b]12;?\x1b\\");
- assert_eq!(r.take(), b"\x1b]12;rgb:cccc/cccc/cccc\x1b\\");
- }
-
- #[test]
- fn no_response_for_regular_output() {
- let r = PtyResponses::new();
- let mut p = make_parser(r.clone());
- p.process(b"hello world\x1b[31mred\x1b[0m");
- assert!(r.take().is_empty());
- }
-
- #[test]
- fn multiple_queries_accumulate() {
- let r = PtyResponses::new();
- let mut p = make_parser(r.clone());
- p.process(b"\x1b[c\x1b[6n");
- let bytes = r.take();
- assert!(bytes.starts_with(b"\x1b[?62;22c"));
- assert!(bytes.ends_with(b"\x1b[1;1R"));
- }
-}