diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 16:24:20 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 16:38:50 +0300 |
| commit | b2e6929aa8199574565fd4adf1e631c68d479e6c (patch) | |
| tree | 99964965fe84f9ea31a0dec7ede2393631dc7f50 /src | |
| parent | 822c3b9e2d1edb7481fa558497dd33c7f5dca9ee (diff) | |
refactor: remove vt100 backend and make ghostty default
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/mod.rs | 3 | ||||
| -rw-r--r-- | src/detect.rs | 26 | ||||
| -rw-r--r-- | src/ghostty/mod.rs | 1 | ||||
| -rw-r--r-- | src/input/encode.rs | 4 | ||||
| -rw-r--r-- | src/input/mod.rs | 1 | ||||
| -rw-r--r-- | src/main.rs | 2 | ||||
| -rw-r--r-- | src/pane.rs | 539 | ||||
| -rw-r--r-- | src/pty_callbacks.rs | 450 | ||||
| -rw-r--r-- | src/selection.rs | 7 | ||||
| -rw-r--r-- | src/terminal_theme.rs | 1 |
10 files changed, 49 insertions, 985 deletions
diff --git a/src/app/mod.rs b/src/app/mod.rs index 064b153a..65a1cc4a 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -774,12 +774,10 @@ impl App { return; } - #[cfg(feature = "ghostty-vt")] if let Some((ws_idx, pane_id)) = self.last_focus { self.send_pane_focus_event(ws_idx, pane_id, crate::ghostty::FocusEvent::Lost); } if let Some((ws_idx, pane_id)) = current_focus { - #[cfg(feature = "ghostty-vt")] self.send_pane_focus_event(ws_idx, pane_id, crate::ghostty::FocusEvent::Gained); self.emit_event(crate::api::schema::EventEnvelope { event: crate::api::schema::EventKind::WorkspaceFocused, @@ -812,7 +810,6 @@ impl App { self.last_focus = current_focus; } - #[cfg(feature = "ghostty-vt")] fn send_pane_focus_event( &self, ws_idx: usize, diff --git a/src/detect.rs b/src/detect.rs index bc59fe25..b7ffa441 100644 --- a/src/detect.rs +++ b/src/detect.rs @@ -1,6 +1,6 @@ //! Agent state detection via screen content pattern matching. //! -//! Each pane's `vt100::Screen` content is read periodically and matched +//! Each pane's visible terminal content is read periodically and matched //! against known agent output patterns to determine state. /// The detected state of a terminal pane. @@ -1177,31 +1177,19 @@ mod tests { let _ = tpgid; } - // ---- VT100 integration ---- - #[test] - fn vt100_screen_content_works_with_detection() { - let mut parser = vt100::Parser::new(24, 80, 0); - parser.process(b"Working..."); - let content = parser.screen().contents(); - assert_eq!(detect_pi(&content), AgentState::Working); + fn visible_screen_content_works_with_detection() { + assert_eq!(detect_pi("Working..."), AgentState::Working); } #[test] - fn vt100_screen_with_ansi_colors() { - let mut parser = vt100::Parser::new(24, 80, 0); - // Red "Working..." with ANSI codes — contents() strips formatting - parser.process(b"\x1b[31mWorking...\x1b[0m"); - let content = parser.screen().contents(); - assert_eq!(detect_pi(&content), AgentState::Working); + fn ansi_colored_content_still_detects_working() { + assert_eq!(detect_pi("\x1b[31mWorking...\x1b[0m"), AgentState::Working); } #[test] - fn vt100_screen_claude_prompt_box() { - let mut parser = vt100::Parser::new(24, 80, 0); + fn visible_claude_prompt_box_is_idle() { let screen = "Task complete.\n─────────────\n❯ \n─────────────"; - parser.process(screen.as_bytes()); - let content = parser.screen().contents(); - assert_eq!(detect_claude(&content), AgentState::Idle); + assert_eq!(detect_claude(screen), AgentState::Idle); } } diff --git a/src/ghostty/mod.rs b/src/ghostty/mod.rs index c5214472..e2bd409c 100644 --- a/src/ghostty/mod.rs +++ b/src/ghostty/mod.rs @@ -1,4 +1,3 @@ -#![cfg(feature = "ghostty-vt")] #![allow(dead_code)] #[allow( diff --git a/src/input/encode.rs b/src/input/encode.rs index 610cc694..4aa68e17 100644 --- a/src/input/encode.rs +++ b/src/input/encode.rs @@ -39,6 +39,7 @@ pub fn encode_cursor_key(code: KeyCode, application_cursor: bool) -> Vec<u8> { } } +#[allow(dead_code)] // exercised in input unit tests; pane runtime uses backend helpers pub fn encode_mouse_scroll( kind: MouseEventKind, column: u16, @@ -56,6 +57,7 @@ pub fn encode_mouse_scroll( encode_mouse_cb(button, false, column, row, modifiers, encoding) } +#[allow(dead_code)] // exercised in input unit tests; pane runtime uses backend helpers pub fn encode_mouse_button( kind: MouseEventKind, column: u16, @@ -78,6 +80,7 @@ pub fn encode_mouse_button( encode_mouse_cb(button, release, column, row, modifiers, encoding) } +#[allow(dead_code)] // only reached through mouse encoding helpers above fn encode_mouse_cb( base_button: u16, release: bool, @@ -129,6 +132,7 @@ fn encode_mouse_cb( } } +#[allow(dead_code)] // only reached through mouse encoding helpers above fn push_mouse_codepoint(bytes: &mut Vec<u8>, value: u32) -> Option<()> { let ch = char::from_u32(value)?; let mut buf = [0u8; 4]; diff --git a/src/input/mod.rs b/src/input/mod.rs index 4e34674c..109fd312 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -4,6 +4,7 @@ mod parse; #[cfg(test)] pub use encode::encode_key; +#[allow(unused_imports)] pub use encode::{ encode_cursor_key, encode_mouse_button, encode_mouse_scroll, encode_terminal_key, }; diff --git a/src/main.rs b/src/main.rs index b0f986bd..4fdfc9de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,7 +24,6 @@ mod cli; mod config; mod detect; mod events; -#[cfg(feature = "ghostty-vt")] mod ghostty; mod input; mod integration; @@ -32,7 +31,6 @@ mod layout; mod pane; mod persist; mod platform; -mod pty_callbacks; mod raw_input; mod release_notes; mod selection; diff --git a/src/pane.rs b/src/pane.rs index f71822bd..02bf5033 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -2,24 +2,20 @@ use std::cell::Cell; use std::io::{BufWriter, Read, Write}; use std::sync::{ atomic::{AtomicBool, AtomicU16, AtomicU32, Ordering}, - Arc, Mutex, RwLock, + Arc, Mutex, }; use bytes::Bytes; use portable_pty::{native_pty_system, CommandBuilder, PtySize}; -#[cfg(feature = "ghostty-vt")] use ratatui::style::{Color, Modifier, Style}; use ratatui::{layout::Rect, Frame}; use tokio::sync::{mpsc, Notify}; use tracing::{debug, error, info, warn}; -use tui_term::widget::PseudoTerminal; -#[cfg(feature = "ghostty-vt")] use unicode_width::UnicodeWidthStr; use crate::detect::{Agent, AgentState}; use crate::events::AppEvent; use crate::layout::PaneId; -use crate::pty_callbacks::PtyResponses; const CLAUDE_WORKING_HOLD: std::time::Duration = std::time::Duration::from_millis(1200); const RELEASE_REACQUIRE_SUPPRESSION: std::time::Duration = std::time::Duration::from_secs(1); @@ -289,29 +285,17 @@ struct ProcessBytesResult { request_render: bool, } -#[cfg_attr(feature = "ghostty-vt", allow(dead_code))] -struct VtPaneTerminal { - parser: Arc<RwLock<vt100::Parser<PtyResponses>>>, - screen_content: Arc<RwLock<String>>, - mouse_alternate_scroll: Arc<AtomicBool>, -} - -#[cfg(feature = "ghostty-vt")] struct GhosttyPaneTerminal { core: Mutex<GhosttyPaneCore>, } -#[cfg(feature = "ghostty-vt")] struct GhosttyPaneCore { terminal: crate::ghostty::Terminal, render_state: crate::ghostty::RenderState, } -enum PaneTerminal { - #[cfg_attr(feature = "ghostty-vt", allow(dead_code))] - Vt(VtPaneTerminal), - #[cfg(feature = "ghostty-vt")] - Ghostty(GhosttyPaneTerminal), +struct PaneTerminal { + ghostty: GhosttyPaneTerminal, } impl PaneTerminal { @@ -321,119 +305,63 @@ impl PaneTerminal { bytes: &[u8], response_writer: &mpsc::Sender<Bytes>, ) -> ProcessBytesResult { - match self { - Self::Vt(vt) => vt.process_pty_bytes(pane_id, bytes, response_writer), - #[cfg(feature = "ghostty-vt")] - Self::Ghostty(ghostty) => ghostty.process_pty_bytes(pane_id, bytes, response_writer), - } + self.ghostty + .process_pty_bytes(pane_id, bytes, response_writer) } fn resize(&self, rows: u16, cols: u16) { - match self { - Self::Vt(vt) => vt.resize(rows, cols), - #[cfg(feature = "ghostty-vt")] - Self::Ghostty(ghostty) => ghostty.resize(rows, cols), - } + self.ghostty.resize(rows, cols); } fn scroll_up(&self, lines: usize) { - match self { - Self::Vt(vt) => vt.scroll_up(lines), - #[cfg(feature = "ghostty-vt")] - Self::Ghostty(ghostty) => ghostty.scroll_up(lines), - } + self.ghostty.scroll_up(lines); } fn scroll_down(&self, lines: usize) { - match self { - Self::Vt(vt) => vt.scroll_down(lines), - #[cfg(feature = "ghostty-vt")] - Self::Ghostty(ghostty) => ghostty.scroll_down(lines), - } + self.ghostty.scroll_down(lines); } fn scroll_reset(&self) { - match self { - Self::Vt(vt) => vt.scroll_reset(), - #[cfg(feature = "ghostty-vt")] - Self::Ghostty(ghostty) => ghostty.scroll_reset(), - } + self.ghostty.scroll_reset(); } fn set_scroll_offset_from_bottom(&self, lines: usize) { - match self { - Self::Vt(vt) => vt.set_scroll_offset_from_bottom(lines), - #[cfg(feature = "ghostty-vt")] - Self::Ghostty(ghostty) => ghostty.set_scroll_offset_from_bottom(lines), - } + self.ghostty.set_scroll_offset_from_bottom(lines); } fn scroll_metrics(&self) -> Option<ScrollMetrics> { - match self { - Self::Vt(vt) => vt.scroll_metrics(), - #[cfg(feature = "ghostty-vt")] - Self::Ghostty(ghostty) => ghostty.scroll_metrics(), - } + self.ghostty.scroll_metrics() } fn input_state(&self) -> Option<InputState> { - match self { - Self::Vt(vt) => vt.input_state(), - #[cfg(feature = "ghostty-vt")] - Self::Ghostty(ghostty) => ghostty.input_state(), - } + self.ghostty.input_state() } fn visible_text(&self) -> String { - match self { - Self::Vt(vt) => vt.visible_text(), - #[cfg(feature = "ghostty-vt")] - Self::Ghostty(ghostty) => ghostty.visible_text(), - } + self.ghostty.visible_text() } fn recent_text(&self, lines: usize) -> String { - match self { - Self::Vt(vt) => vt.recent_text(lines), - #[cfg(feature = "ghostty-vt")] - Self::Ghostty(ghostty) => ghostty.recent_text(lines), - } + self.ghostty.recent_text(lines) } fn extract_selection(&self, selection: &crate::selection::Selection) -> Option<String> { - match self { - Self::Vt(vt) => vt.extract_selection(selection), - #[cfg(feature = "ghostty-vt")] - Self::Ghostty(ghostty) => ghostty.extract_selection(selection), - } + self.ghostty.extract_selection(selection) } fn render(&self, frame: &mut Frame, area: Rect) { - match self { - Self::Vt(vt) => vt.render(frame, area), - #[cfg(feature = "ghostty-vt")] - Self::Ghostty(ghostty) => ghostty.render(frame, area), - } + self.ghostty.render(frame, area); } fn apply_host_terminal_theme(&self, theme: crate::terminal_theme::TerminalTheme) { - let _ = theme; - match self { - Self::Vt(_) => {} - #[cfg(feature = "ghostty-vt")] - Self::Ghostty(ghostty) => ghostty.apply_host_terminal_theme(theme), - } + self.ghostty.apply_host_terminal_theme(theme); } fn keyboard_protocol( &self, fallback: crate::input::KeyboardProtocol, ) -> crate::input::KeyboardProtocol { - match self { - Self::Vt(_) => fallback, - #[cfg(feature = "ghostty-vt")] - Self::Ghostty(ghostty) => ghostty.keyboard_protocol().unwrap_or(fallback), - } + self.ghostty.keyboard_protocol().unwrap_or(fallback) } fn encode_terminal_key( @@ -441,11 +369,7 @@ impl PaneTerminal { key: crate::input::TerminalKey, protocol: crate::input::KeyboardProtocol, ) -> Vec<u8> { - match self { - Self::Vt(vt) => vt.encode_terminal_key(key, protocol), - #[cfg(feature = "ghostty-vt")] - Self::Ghostty(ghostty) => ghostty.encode_terminal_key(key, protocol), - } + self.ghostty.encode_terminal_key(key, protocol) } fn encode_mouse_button( @@ -455,19 +379,8 @@ impl PaneTerminal { row: u16, modifiers: crossterm::event::KeyModifiers, ) -> Option<Vec<u8>> { - match self { - Self::Vt(vt) => vt.input_state().and_then(|input_state| { - crate::input::encode_mouse_button( - kind, - column, - row, - modifiers, - input_state.mouse_protocol_encoding, - ) - }), - #[cfg(feature = "ghostty-vt")] - Self::Ghostty(ghostty) => ghostty.encode_mouse_button(kind, column, row, modifiers), - } + self.ghostty + .encode_mouse_button(kind, column, row, modifiers) } fn encode_mouse_wheel( @@ -477,219 +390,11 @@ impl PaneTerminal { row: u16, modifiers: crossterm::event::KeyModifiers, ) -> Option<Vec<u8>> { - match self { - Self::Vt(vt) => vt.input_state().and_then(|input_state| { - crate::input::encode_mouse_scroll( - kind, - column, - row, - modifiers, - input_state.mouse_protocol_encoding, - ) - }), - #[cfg(feature = "ghostty-vt")] - Self::Ghostty(ghostty) => ghostty.encode_mouse_wheel(kind, column, row, modifiers), - } - } -} - -#[cfg_attr(feature = "ghostty-vt", allow(dead_code))] -impl VtPaneTerminal { - fn new( - parser: Arc<RwLock<vt100::Parser<PtyResponses>>>, - screen_content: Arc<RwLock<String>>, - mouse_alternate_scroll: Arc<AtomicBool>, - ) -> Self { - Self { - parser, - screen_content, - mouse_alternate_scroll, - } - } - - fn process_pty_bytes( - &self, - pane_id: PaneId, - bytes: &[u8], - response_writer: &mpsc::Sender<Bytes>, - ) -> ProcessBytesResult { - let resp = if let Ok(mut parser) = self.parser.write() { - parser.process(bytes); - - let scrollback = parser.screen().scrollback(); - if scrollback > 0 { - parser.screen_mut().set_scrollback(0); - } - let content = parser.screen().contents(); - if scrollback > 0 { - parser.screen_mut().set_scrollback(scrollback); - } - if let Ok(mut screen_content) = self.screen_content.write() { - *screen_content = content; - } - parser.callbacks_mut().take() - } else { - error!(pane = pane_id.raw(), "parser lock poisoned in reader"); - return ProcessBytesResult { - request_render: false, - }; - }; - - if !resp.is_empty() { - if let Err(e) = response_writer.try_send(Bytes::from(resp)) { - warn!(pane = pane_id.raw(), err = %e, "dropped terminal query response"); - } - } - - ProcessBytesResult { - request_render: true, - } - } - - fn resize(&self, rows: u16, cols: u16) { - if let Ok(mut parser) = self.parser.write() { - parser.screen_mut().set_size(rows, cols); - } - } - - fn scroll_up(&self, lines: usize) { - if let Ok(mut parser) = self.parser.write() { - let current = parser.screen().scrollback(); - parser.screen_mut().set_scrollback(current + lines); - } - } - - fn scroll_down(&self, lines: usize) { - if let Ok(mut parser) = self.parser.write() { - let current = parser.screen().scrollback(); - parser - .screen_mut() - .set_scrollback(current.saturating_sub(lines)); - } - } - - fn scroll_reset(&self) { - if let Ok(mut parser) = self.parser.write() { - parser.screen_mut().set_scrollback(0); - } - } - - fn set_scroll_offset_from_bottom(&self, lines: usize) { - if let Ok(mut parser) = self.parser.write() { - parser.screen_mut().set_scrollback(lines); - } - } - - fn scroll_metrics(&self) -> Option<ScrollMetrics> { - let Ok(mut parser) = self.parser.write() else { - return None; - }; - let max_offset_from_bottom = max_scrollback(&mut parser); - let screen = parser.screen(); - let (viewport_rows, _) = screen.size(); - Some(ScrollMetrics { - offset_from_bottom: screen.scrollback(), - max_offset_from_bottom, - viewport_rows: viewport_rows as usize, - }) - } - - fn input_state(&self) -> Option<InputState> { - let Ok(parser) = self.parser.read() else { - return None; - }; - let screen = parser.screen(); - Some(InputState { - alternate_screen: screen.alternate_screen(), - application_cursor: screen.application_cursor(), - bracketed_paste: screen.bracketed_paste(), - focus_reporting: false, - mouse_protocol_mode: match screen.mouse_protocol_mode() { - vt100::MouseProtocolMode::None => crate::input::MouseProtocolMode::None, - vt100::MouseProtocolMode::Press => crate::input::MouseProtocolMode::Press, - vt100::MouseProtocolMode::PressRelease => { - crate::input::MouseProtocolMode::PressRelease - } - vt100::MouseProtocolMode::ButtonMotion => { - crate::input::MouseProtocolMode::ButtonMotion - } - vt100::MouseProtocolMode::AnyMotion => crate::input::MouseProtocolMode::AnyMotion, - }, - mouse_protocol_encoding: match screen.mouse_protocol_encoding() { - vt100::MouseProtocolEncoding::Default => { - crate::input::MouseProtocolEncoding::Default - } - vt100::MouseProtocolEncoding::Utf8 => crate::input::MouseProtocolEncoding::Utf8, - vt100::MouseProtocolEncoding::Sgr => crate::input::MouseProtocolEncoding::Sgr, - }, - mouse_alternate_scroll: self.mouse_alternate_scroll.load(Ordering::Relaxed), - }) - } - - fn visible_text(&self) -> String { - let Ok(content) = self.screen_content.read() else { - return String::new(); - }; - let mut rows: Vec<String> = content - .lines() - .map(|line| line.trim_end().to_string()) - .collect(); - trim_trailing_blank_rows(&mut rows); - let text = rows.join("\n"); - if text.is_empty() { - text - } else { - format!("{text}\n") - } - } - - fn recent_text(&self, lines: usize) -> String { - self.parser - .write() - .map(|mut parser| recent_text_from_parser(&mut parser, lines)) - .unwrap_or_default() - } - - fn extract_selection(&self, selection: &crate::selection::Selection) -> Option<String> { - self.parser - .read() - .map(|parser| crate::selection::extract_text(parser.screen(), selection)) - .ok() - } - - fn render(&self, frame: &mut Frame, area: Rect) { - if let Ok(parser) = self.parser.read() { - let show_cursor = parser.screen().scrollback() == 0; - let pt = PseudoTerminal::new(parser.screen()) - .cursor(tui_term::widget::Cursor::default().visibility(show_cursor)); - frame.render_widget(pt, area); - } - } - - fn encode_terminal_key( - &self, - key: crate::input::TerminalKey, - protocol: crate::input::KeyboardProtocol, - ) -> Vec<u8> { - let application_cursor = self - .input_state() - .map(|state| state.application_cursor) - .unwrap_or(false); - if matches!( - key.code, - crossterm::event::KeyCode::Up - | crossterm::event::KeyCode::Down - | crossterm::event::KeyCode::Left - | crossterm::event::KeyCode::Right - ) && key.modifiers.is_empty() - { - return crate::input::encode_cursor_key(key.code, application_cursor); - } - crate::input::encode_terminal_key(key, protocol) + self.ghostty + .encode_mouse_wheel(kind, column, row, modifiers) } } -#[cfg(feature = "ghostty-vt")] impl GhosttyPaneTerminal { fn new( mut terminal: crate::ghostty::Terminal, @@ -1019,8 +724,6 @@ impl GhosttyPaneTerminal { } } -#[cfg(feature = "ghostty-vt")] -#[cfg(feature = "ghostty-vt")] fn ghostty_key_event_from_terminal_key( key: crate::input::TerminalKey, ) -> Option<crate::ghostty::KeyEvent> { @@ -1055,12 +758,10 @@ fn ghostty_key_event_from_terminal_key( Some(event) } -#[cfg(feature = "ghostty-vt")] fn ghostty_prefers_herdr_text_encoding(key: crate::input::TerminalKey) -> bool { matches!(key.code, crossterm::event::KeyCode::Char(_)) } -#[cfg(feature = "ghostty-vt")] fn ghostty_mods_from_key_modifiers(modifiers: crossterm::event::KeyModifiers) -> u16 { let mut ghostty_mods = 0u16; if modifiers.contains(crossterm::event::KeyModifiers::SHIFT) { @@ -1078,7 +779,6 @@ fn ghostty_mods_from_key_modifiers(modifiers: crossterm::event::KeyModifiers) -> ghostty_mods } -#[cfg(feature = "ghostty-vt")] fn ghostty_mouse_encoder_for_terminal( terminal: &crate::ghostty::Terminal, ) -> Option<crate::ghostty::MouseEncoder> { @@ -1090,7 +790,6 @@ fn ghostty_mouse_encoder_for_terminal( Some(encoder) } -#[cfg(feature = "ghostty-vt")] fn ghostty_mouse_event_from_button_kind( kind: crossterm::event::MouseEventKind, column: u16, @@ -1148,7 +847,6 @@ fn ghostty_mouse_event_from_button_kind( Some(event) } -#[cfg(feature = "ghostty-vt")] fn ghostty_mouse_event_from_wheel_kind( kind: crossterm::event::MouseEventKind, column: u16, @@ -1170,7 +868,6 @@ fn ghostty_mouse_event_from_wheel_kind( Some(event) } -#[cfg(feature = "ghostty-vt")] fn ghostty_key_text(key: crate::input::TerminalKey) -> Option<String> { match key.code { crossterm::event::KeyCode::Char(c) => Some( @@ -1183,7 +880,6 @@ fn ghostty_key_text(key: crate::input::TerminalKey) -> Option<String> { } } -#[cfg(feature = "ghostty-vt")] fn ghostty_unshifted_codepoint(key: crate::input::TerminalKey) -> Option<u32> { match key.code { crossterm::event::KeyCode::Char(c) => Some(c as u32), @@ -1191,7 +887,6 @@ fn ghostty_unshifted_codepoint(key: crate::input::TerminalKey) -> Option<u32> { } } -#[cfg(feature = "ghostty-vt")] fn ghostty_key_from_crossterm_key_code( code: crossterm::event::KeyCode, shifted_codepoint: Option<u32>, @@ -1234,7 +929,6 @@ fn ghostty_key_from_crossterm_key_code( } } -#[cfg(feature = "ghostty-vt")] fn ghostty_key_from_char(c: char, shifted_codepoint: Option<u32>) -> Option<u32> { use crate::ghostty::ffi; @@ -1297,7 +991,6 @@ fn ghostty_key_from_char(c: char, shifted_codepoint: Option<u32>) -> Option<u32> } } -#[cfg(feature = "ghostty-vt")] fn ghostty_unshifted_ascii_pair(c: char) -> Option<char> { Some(match c { '!' => '1', @@ -1325,7 +1018,6 @@ fn ghostty_unshifted_ascii_pair(c: char) -> Option<char> { }) } -#[cfg(feature = "ghostty-vt")] fn ghostty_visible_text(core: &mut GhosttyPaneCore) -> Result<String, crate::ghostty::Error> { let GhosttyPaneCore { terminal, @@ -1344,7 +1036,6 @@ fn ghostty_visible_text(core: &mut GhosttyPaneCore) -> Result<String, crate::gho Ok(lines_to_text(lines)) } -#[cfg(feature = "ghostty-vt")] fn ghostty_recent_text( core: &GhosttyPaneCore, lines: usize, @@ -1360,7 +1051,6 @@ fn ghostty_recent_text( Ok(recent_text_from_rows(&rows, lines)) } -#[cfg(feature = "ghostty-vt")] fn ghostty_extract_selection( core: &mut GhosttyPaneCore, selection: &crate::selection::Selection, @@ -1395,7 +1085,6 @@ fn ghostty_extract_selection( Ok(lines.join("\n")) } -#[cfg(feature = "ghostty-vt")] fn selection_cols_for_row(selection: &crate::selection::Selection, row: u16) -> (u16, u16) { let ((start_row, start_col), (end_row, end_col)) = selection.ordered_cells(); if start_row == end_row { @@ -1409,7 +1098,6 @@ fn selection_cols_for_row(selection: &crate::selection::Selection, row: u16) -> } } -#[cfg(feature = "ghostty-vt")] fn ghostty_screen_row( core: &GhosttyPaneCore, cols: u16, @@ -1431,7 +1119,6 @@ fn ghostty_screen_row( Ok(line.trim_end().to_string()) } -#[cfg(feature = "ghostty-vt")] fn ghostty_line_from_cells( cells: &mut crate::ghostty::RowCellIter<'_>, ) -> Result<String, crate::ghostty::Error> { @@ -1442,7 +1129,6 @@ fn ghostty_line_from_cells( Ok(line.trim_end().to_string()) } -#[cfg(feature = "ghostty-vt")] fn ghostty_cell_symbol( cells: &crate::ghostty::RowCellIter<'_>, ) -> Result<String, crate::ghostty::Error> { @@ -1462,7 +1148,6 @@ fn ghostty_cell_symbol( Ok(text) } -#[cfg(feature = "ghostty-vt")] fn ghostty_blank_symbol_for_width(wide: crate::ghostty::CellWide) -> &'static str { match wide { crate::ghostty::CellWide::Wide => " ", @@ -1471,7 +1156,6 @@ fn ghostty_blank_symbol_for_width(wide: crate::ghostty::CellWide) -> &'static st } } -#[cfg(feature = "ghostty-vt")] fn ghostty_normalize_buffer_symbol(symbol: &str, wide: crate::ghostty::CellWide) -> String { let expected_width = match wide { crate::ghostty::CellWide::Wide => 2, @@ -1485,7 +1169,6 @@ fn ghostty_normalize_buffer_symbol(symbol: &str, wide: crate::ghostty::CellWide) ghostty_blank_symbol_for_width(wide).to_string() } -#[cfg(feature = "ghostty-vt")] fn ghostty_buffer_symbol( cells: &crate::ghostty::RowCellIter<'_>, wide: crate::ghostty::CellWide, @@ -1500,7 +1183,6 @@ fn ghostty_buffer_symbol( Ok(ghostty_normalize_buffer_symbol(&symbol, wide)) } -#[cfg(feature = "ghostty-vt")] fn ghostty_reset_cell( cell: &mut ratatui::buffer::Cell, default_fg: Option<Color>, @@ -1516,7 +1198,6 @@ fn ghostty_reset_cell( } } -#[cfg(feature = "ghostty-vt")] fn ghostty_cell_style( cells: &crate::ghostty::RowCellIter<'_>, default_fg: Option<Color>, @@ -1572,7 +1253,6 @@ fn ghostty_cell_style( style.add_modifier(modifiers) } -#[cfg(feature = "ghostty-vt")] fn ghostty_color(color: crate::ghostty::RgbColor) -> Color { Color::Rgb(color.r, color.g, color.b) } @@ -1608,44 +1288,6 @@ fn trim_trailing_blank_rows(rows: &mut Vec<String>) { } } -fn max_scrollback(parser: &mut vt100::Parser<PtyResponses>) -> usize { - let screen = parser.screen_mut(); - let original_scrollback = screen.scrollback(); - screen.set_scrollback(usize::MAX); - let max_scrollback = screen.scrollback(); - screen.set_scrollback(original_scrollback); - max_scrollback -} - -fn parser_rows(parser: &mut vt100::Parser<PtyResponses>, lines: usize) -> Vec<String> { - let max_scrollback = max_scrollback(parser); - let screen = parser.screen_mut(); - let original_scrollback = screen.scrollback(); - - let (_, cols) = screen.size(); - screen.set_scrollback(0); - let visible_rows: Vec<String> = screen - .rows(0, cols) - .map(|row| row.trim_end().to_string()) - .collect(); - let extra_rows = lines.saturating_sub(visible_rows.len()).min(max_scrollback); - - let mut rows = Vec::with_capacity(extra_rows + visible_rows.len()); - if extra_rows > 0 { - for offset in (1..=extra_rows).rev() { - screen.set_scrollback(offset); - if let Some(row) = screen.rows(0, cols).next() { - rows.push(row.trim_end().to_string()); - } - } - } - - screen.set_scrollback(original_scrollback); - rows.extend(visible_rows); - trim_trailing_blank_rows(&mut rows); - rows -} - fn recent_text_from_rows(rows: &[String], lines: usize) -> String { let start = rows.len().saturating_sub(lines); let text = rows[start..].join("\n"); @@ -1656,11 +1298,6 @@ fn recent_text_from_rows(rows: &[String], lines: usize) -> String { } } -fn recent_text_from_parser(parser: &mut vt100::Parser<PtyResponses>, lines: usize) -> String { - let rows = parser_rows(parser, lines); - recent_text_from_rows(&rows, lines) -} - fn wait_for_processes_to_exit(pids: &[u32], timeout: std::time::Duration) -> bool { let deadline = std::time::Instant::now() + timeout; loop { @@ -1756,41 +1393,14 @@ impl PaneRuntime { // --- Writer channel --- let (input_tx, mut input_rx) = mpsc::channel::<Bytes>(32); - // Live screen snapshot for detection (decoupled from parser scrollback) - #[cfg_attr(feature = "ghostty-vt", allow(unused_variables))] - let screen_content = Arc::new(RwLock::new(String::new())); - - #[cfg(not(feature = "ghostty-vt"))] - let terminal = { - let responses = PtyResponses::new(); - let kitty_keyboard_flags = responses.kitty_keyboard_flags.clone(); - let mouse_alternate_scroll = responses.mouse_alternate_scroll.clone(); - let parser = Arc::new(RwLock::new(vt100::Parser::new_with_callbacks( - rows, cols, 10000, responses, - ))); - ( - Arc::new(PaneTerminal::Vt(VtPaneTerminal::new( - parser, - screen_content.clone(), - mouse_alternate_scroll, - ))), - kitty_keyboard_flags, - ) - }; - - #[cfg(feature = "ghostty-vt")] - let terminal = { - let terminal = crate::ghostty::Terminal::new(cols, rows, 10000) - .map_err(|e| std::io::Error::other(e.to_string()))?; - let pane_terminal = GhosttyPaneTerminal::new(terminal, input_tx.clone())?; - pane_terminal.apply_host_terminal_theme(_host_terminal_theme); - ( - Arc::new(PaneTerminal::Ghostty(pane_terminal)), - Arc::new(AtomicU16::new(0)), - ) - }; - - let (terminal, kitty_keyboard_flags) = terminal; + let terminal = crate::ghostty::Terminal::new(cols, rows, 10000) + .map_err(|e| std::io::Error::other(e.to_string()))?; + let pane_terminal = GhosttyPaneTerminal::new(terminal, input_tx.clone())?; + pane_terminal.apply_host_terminal_theme(_host_terminal_theme); + let terminal = Arc::new(PaneTerminal { + ghostty: pane_terminal, + }); + let kitty_keyboard_flags = Arc::new(AtomicU16::new(0)); let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into()); let mut cmd = CommandBuilder::new(&shell); @@ -2141,7 +1751,6 @@ impl PaneRuntime { self.send_bytes(Bytes::from(payload)).await } - #[cfg(feature = "ghostty-vt")] pub fn try_send_focus_event(&self, event: crate::ghostty::FocusEvent) -> bool { if !self .input_state() @@ -2229,7 +1838,6 @@ impl PaneRuntime { mod tests { use super::*; - #[cfg(feature = "ghostty-vt")] #[test] fn ghostty_keyboard_protocol_tracks_live_terminal_flags() { let (tx, _rx) = mpsc::channel(4); @@ -2243,7 +1851,6 @@ mod tests { ); } - #[cfg(feature = "ghostty-vt")] #[test] fn ghostty_plain_text_chars_still_encode_as_text() { let (tx, _rx) = mpsc::channel(4); @@ -2261,7 +1868,6 @@ mod tests { assert_eq!(encoded, b"a"); } - #[cfg(feature = "ghostty-vt")] #[test] fn ghostty_char_keys_still_use_herdr_encoding() { let (tx, _rx) = mpsc::channel(4); @@ -2280,7 +1886,6 @@ mod tests { assert_eq!(encoded, vec![1]); } - #[cfg(feature = "ghostty-vt")] #[test] fn ghostty_key_encoding_honors_application_cursor_mode() { let (tx, _rx) = mpsc::channel(4); @@ -2301,7 +1906,6 @@ mod tests { assert_eq!(encoded, b"\x1bOA"); } - #[cfg(feature = "ghostty-vt")] #[test] fn ghostty_mouse_button_encoding_uses_live_terminal_state() { let (tx, _rx) = mpsc::channel(4); @@ -2319,7 +1923,6 @@ mod tests { assert_eq!(encoded.as_deref(), Some(&b"\x1b[<0;12;10m"[..])); } - #[cfg(feature = "ghostty-vt")] #[test] fn ghostty_mouse_drag_encoding_uses_motion_reporting_state() { let (tx, _rx) = mpsc::channel(4); @@ -2337,7 +1940,6 @@ mod tests { assert_eq!(encoded.as_deref(), Some(&b"\x1b[<36;5;7M"[..])); } - #[cfg(feature = "ghostty-vt")] #[test] fn ghostty_normalize_buffer_symbol_uses_ratatui_width_contract() { assert_eq!( @@ -2362,7 +1964,6 @@ mod tests { ); } - #[cfg(feature = "ghostty-vt")] #[tokio::test] async fn focus_events_are_forwarded_when_enabled() { let (tx, mut rx) = mpsc::channel(4); @@ -2372,9 +1973,9 @@ mod tests { .mode_set(crate::ghostty::MODE_FOCUS_EVENT, true) .unwrap(); let runtime = PaneRuntime { - terminal: Arc::new(PaneTerminal::Ghostty( - GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(), - )), + terminal: Arc::new(PaneTerminal { + ghostty: GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(), + }), sender: tx, resize_tx, current_size: Cell::new((80, 24)), @@ -2389,16 +1990,15 @@ mod tests { assert_eq!(rx.recv().await.unwrap(), Bytes::from_static(b"\x1b[I")); } - #[cfg(feature = "ghostty-vt")] #[tokio::test] async fn focus_events_are_suppressed_when_disabled() { let (tx, mut rx) = mpsc::channel(4); let (resize_tx, _resize_rx) = mpsc::channel(1); let terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap(); let runtime = PaneRuntime { - terminal: Arc::new(PaneTerminal::Ghostty( - GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(), - )), + terminal: Arc::new(PaneTerminal { + ghostty: GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(), + }), sender: tx, resize_tx, current_size: Cell::new((80, 24)), @@ -2418,29 +2018,6 @@ mod tests { } #[test] - fn recent_text_reconstructs_scrollback_tail() { - let responses = PtyResponses::new(); - let mut parser = vt100::Parser::new_with_callbacks(3, 10, 100, responses); - parser.process(b"a\r\nb\r\nc\r\nd\r\ne"); - - let recent = recent_text_from_parser(&mut parser, 4); - assert_eq!(recent, "b\nc\nd\ne\n"); - } - - #[test] - fn max_scrollback_reports_clamped_history_without_changing_position() { - let responses = PtyResponses::new(); - let mut parser = vt100::Parser::new_with_callbacks(3, 10, 100, responses); - parser.process(b"a\r\nb\r\nc\r\nd\r\ne"); - parser.screen_mut().set_scrollback(1); - - let max = max_scrollback(&mut parser); - - assert_eq!(max, 2); - assert_eq!(parser.screen().scrollback(), 1); - } - - #[test] fn trim_trailing_blank_rows_drops_empty_viewport_tail() { let mut rows = vec!["hello".to_string(), "".to_string(), " ".to_string()]; trim_trailing_blank_rows(&mut rows); @@ -2448,48 +2025,6 @@ mod tests { } #[test] - fn alternate_screen_does_not_accumulate_host_scrollback() { - let responses = PtyResponses::new(); - let mut parser = vt100::Parser::new_with_callbacks(3, 10, 100, responses); - parser.process(b"\x1b[?1049h1\r\n2\r\n3\r\n4"); - - assert!(parser.screen().alternate_screen()); - assert_eq!(max_scrollback(&mut parser), 0); - assert_eq!(recent_text_from_parser(&mut parser, 4), "2\n3\n4\n"); - } - - #[test] - fn normal_screen_top_anchored_scroll_regions_feed_scrollback() { - let responses = PtyResponses::new(); - let mut parser = vt100::Parser::new_with_callbacks(5, 10, 100, responses); - parser.process(b"1\r\n2\r\n3\r\n4\r\n5"); - parser.process(b"\x1b[1;3r\x1b[3;1H\r\nX"); - - assert_eq!(max_scrollback(&mut parser), 1); - } - - #[test] - fn normal_screen_non_top_anchored_scroll_regions_do_not_feed_scrollback() { - let responses = PtyResponses::new(); - let mut parser = vt100::Parser::new_with_callbacks(5, 10, 100, responses); - parser.process(b"1\r\n2\r\n3\r\n4\r\n5"); - parser.process(b"\x1b[2;4r\x1b[4;1H\r\nX"); - - assert_eq!(max_scrollback(&mut parser), 0); - } - - #[test] - fn alternate_screen_scroll_regions_do_not_create_host_scrollback() { - let responses = PtyResponses::new(); - let mut parser = vt100::Parser::new_with_callbacks(5, 10, 100, responses); - parser.process(b"\x1b[?1049h1\r\n2\r\n3\r\n4\r\n5"); - parser.process(b"\x1b[1;3r\x1b[3;1H\r\nX"); - - assert!(parser.screen().alternate_screen()); - assert_eq!(max_scrollback(&mut parser), 0); - } - - #[test] fn claude_working_is_sticky_for_short_gap() { let now = std::time::Instant::now(); let mut last_working = None; 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")); - } -} diff --git a/src/selection.rs b/src/selection.rs index 78c9a2f3..5531a189 100644 --- a/src/selection.rs +++ b/src/selection.rs @@ -142,13 +142,6 @@ impl Selection { } } -/// Extract selected text from a vt100 screen. -pub fn extract_text(screen: &vt100::Screen, selection: &Selection) -> String { - let ((sr, sc), (er, ec)) = selection.ordered(); - // contents_between end_col is exclusive in vt100 - screen.contents_between(sr, sc, er, ec + 1) -} - /// Write text to the system clipboard via OSC 52. /// /// OSC 52 format: `ESC ] 52 ; c ; <base64> BEL` diff --git a/src/terminal_theme.rs b/src/terminal_theme.rs index e0b4b9d5..523e3e38 100644 --- a/src/terminal_theme.rs +++ b/src/terminal_theme.rs @@ -47,7 +47,6 @@ pub fn parse_default_color_response(sequence: &str) -> Option<(DefaultColorKind, Some((kind, parse_rgb_color(value)?)) } -#[cfg_attr(not(feature = "ghostty-vt"), allow(dead_code))] pub fn osc_set_default_color_sequence(kind: DefaultColorKind, color: RgbColor) -> String { let command = match kind { DefaultColorKind::Foreground => 10, |
