From 7245aa6c128a475853ac5ffe95c563dcd38bec94 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Thu, 9 Apr 2026 02:20:49 +0300 Subject: fix: only show cursor in focused pane --- src/pane.rs | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++------ src/ui.rs | 44 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 95 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/pane.rs b/src/pane.rs index 3476b387..9552e1c6 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -354,8 +354,8 @@ impl PaneTerminal { self.ghostty.extract_selection(selection) } - fn render(&self, frame: &mut Frame, area: Rect) { - self.ghostty.render(frame, area); + fn render(&self, frame: &mut Frame, area: Rect, show_cursor: bool) { + self.ghostty.render(frame, area, show_cursor); } fn apply_host_terminal_theme(&self, theme: crate::terminal_theme::TerminalTheme) { @@ -669,7 +669,7 @@ impl GhosttyPaneTerminal { .and_then(|mut core| ghostty_extract_selection(&mut core, selection).ok()) } - fn render(&self, frame: &mut Frame, area: Rect) { + fn render(&self, frame: &mut Frame, area: Rect, show_cursor: bool) { let Ok(mut core) = self.core.lock() else { return; }; @@ -732,7 +732,7 @@ impl GhosttyPaneTerminal { } } - if render_state.cursor_visible().ok() == Some(true) { + if show_cursor && render_state.cursor_visible().ok() == Some(true) { if let Ok(Some(cursor)) = render_state.cursor_viewport() { if cursor.x < area.width && cursor.y < area.height { frame.set_cursor_position((area.x + cursor.x, area.y + cursor.y)); @@ -1722,8 +1722,8 @@ impl PaneRuntime { self.terminal.extract_selection(selection) } - pub fn render(&self, frame: &mut Frame, area: Rect) { - self.terminal.render(frame, area); + pub fn render(&self, frame: &mut Frame, area: Rect, show_cursor: bool) { + self.terminal.render(frame, area, show_cursor); } pub fn keyboard_protocol(&self) -> crate::input::KeyboardProtocol { @@ -1842,6 +1842,30 @@ impl PaneRuntime { } } +#[cfg(test)] +impl PaneRuntime { + pub(crate) fn test_with_screen_bytes(cols: u16, rows: u16, bytes: &[u8]) -> Self { + let (tx, _rx) = mpsc::channel(4); + let (resize_tx, _resize_rx) = mpsc::channel(1); + let mut terminal = crate::ghostty::Terminal::new(cols, rows, 0).unwrap(); + terminal.write(bytes); + + Self { + terminal: Arc::new(PaneTerminal { + ghostty: GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(), + }), + sender: tx, + resize_tx, + current_size: Cell::new((rows, cols)), + child_pid: Arc::new(AtomicU32::new(0)), + kitty_keyboard_flags: Arc::new(AtomicU16::new(0)), + detect_reset_notify: Arc::new(Notify::new()), + pending_release: Arc::new(Mutex::new(None)), + detect_handle: tokio::spawn(async {}).abort_handle(), + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -1852,6 +1876,29 @@ mod tests { } } + #[test] + fn ghostty_render_can_suppress_cursor_position() { + let (tx, _rx) = mpsc::channel(4); + let mut first_terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap(); + first_terminal.write(b"left"); + let first = GhosttyPaneTerminal::new(first_terminal, tx.clone()).unwrap(); + + let mut second_terminal = crate::ghostty::Terminal::new(20, 5, 0).unwrap(); + second_terminal.write(b"r\r\nb"); + let second = GhosttyPaneTerminal::new(second_terminal, tx).unwrap(); + + let backend = ratatui::backend::TestBackend::new(40, 5); + let mut terminal = ratatui::Terminal::new(backend).unwrap(); + terminal + .draw(|frame| { + first.render(frame, Rect::new(0, 0, 20, 5), true); + second.render(frame, Rect::new(20, 0, 20, 5), false); + }) + .unwrap(); + + terminal.backend_mut().assert_cursor_position((4, 0)); + } + #[test] fn ghostty_keyboard_protocol_tracks_live_terminal_flags() { let (tx, _rx) = mpsc::channel(4); diff --git a/src/ui.rs b/src/ui.rs index 4540f20f..8077cc61 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1207,8 +1207,8 @@ fn render_panes(app: &AppState, frame: &mut Frame, area: Rect) { frame.render_widget(block, info.rect); } - // Draw terminal content - rt.render(frame, info.inner_rect); + // Draw terminal content. Only the focused pane should own the cursor. + rt.render(frame, info.inner_rect, info.is_focused && terminal_active); render_pane_scrollbar(app, frame, info, rt); // Dim unfocused panes only in navigate mode @@ -3029,6 +3029,46 @@ fn _build_hints(items: &[(&str, &str)], key_style: Style, dim_style: Style) -> V mod tests { use super::*; use crate::{detect::Agent, workspace::Workspace}; + use ratatui::{backend::TestBackend, Terminal}; + + #[tokio::test] + async fn focused_pane_cursor_wins_during_terminal_render() { + let mut app = crate::app::state::AppState::test_new(); + let mut ws = Workspace::test_new("test"); + let first_pane = ws.tabs[0].root_pane; + let second_pane = ws.test_split(ratatui::layout::Direction::Horizontal); + + ws.tabs[0].runtimes.insert( + first_pane, + crate::pane::PaneRuntime::test_with_screen_bytes(20, 5, b"left"), + ); + ws.tabs[0].runtimes.insert( + second_pane, + crate::pane::PaneRuntime::test_with_screen_bytes(20, 5, b"r\r\nb"), + ); + ws.tabs[0].layout.focus_pane(first_pane); + + app.workspaces = vec![ws]; + app.active = Some(0); + app.selected = 0; + app.mode = Mode::Terminal; + + compute_view(&mut app, Rect::new(0, 0, 80, 20)); + let focused = app + .view + .pane_infos + .iter() + .find(|info| info.id == first_pane) + .expect("focused pane info"); + + let backend = TestBackend::new(80, 20); + let mut terminal = Terminal::new(backend).unwrap(); + terminal.draw(|frame| render(&app, frame)).unwrap(); + + terminal + .backend_mut() + .assert_cursor_position((focused.inner_rect.x + 4, focused.inner_rect.y)); + } #[test] fn all_workspaces_agent_panel_entries_use_workspace_and_optional_tab_labels() { -- cgit v1.2.3