diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-08 01:36:51 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-08 01:36:51 +0300 |
| commit | fb56e64537de5f93786ed121f703da2a4105badc (patch) | |
| tree | 67f6e100141dbee05c75ec421927d62e54d23ec6 /src/server | |
| parent | 416827872a6e2c820bd58209d267685d4c07cb7d (diff) | |
fix: improve windows pane behavior
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/headless.rs | 64 | ||||
| -rw-r--r-- | src/server/render_stream.rs | 87 |
2 files changed, 145 insertions, 6 deletions
diff --git a/src/server/headless.rs b/src/server/headless.rs index f313161f..0f9f3bb6 100644 --- a/src/server/headless.rs +++ b/src/server/headless.rs @@ -4869,6 +4869,70 @@ next_tab = "" } #[tokio::test] + async fn virtual_render_hides_focused_pane_cursor_during_synchronized_output() { + let mut state = AppState::test_new(); + state.reveal_hidden_cursor_for_cjk_ime = true; + let mut ws = crate::workspace::Workspace::test_new("test"); + let pane_id = ws.tabs[0].root_pane; + let runtime = crate::terminal::TerminalRuntime::test_with_screen_bytes(20, 5, b"left"); + ws.insert_test_runtime(pane_id, runtime); + + state.workspaces = vec![ws]; + state.active = Some(0); + state.selected = 0; + state.mode = crate::app::Mode::Terminal; + + let area = Rect::new(0, 0, 80, 24); + let _ = crate::server::render_stream::render_virtual(&mut state, area, true); + let terminal_runtimes = crate::terminal::TerminalRuntimeRegistry::new(); + let runtime = state + .runtime_for_pane(&terminal_runtimes, pane_id) + .expect("pane runtime after initial render"); + runtime.test_process_pty_bytes(b"\x1b[?2026h\x1b[2;3H"); + assert!(runtime.synchronized_output_active()); + + let (_buffer, cursor) = + crate::server::render_stream::render_virtual(&mut state, area, false); + + assert_eq!( + cursor, None, + "child cursor positions are unstable while synchronized output is active" + ); + } + + #[tokio::test] + async fn virtual_render_hides_focused_pane_cursor_during_synchronized_output_resize() { + let mut state = AppState::test_new(); + let mut ws = crate::workspace::Workspace::test_new("test"); + let pane_id = ws.tabs[0].root_pane; + let runtime = crate::terminal::TerminalRuntime::test_with_screen_bytes(20, 5, b"left"); + ws.insert_test_runtime(pane_id, runtime); + + state.workspaces = vec![ws]; + state.active = Some(0); + state.selected = 0; + state.mode = crate::app::Mode::Terminal; + + let initial_area = Rect::new(0, 0, 80, 24); + let _ = crate::server::render_stream::render_virtual(&mut state, initial_area, true); + let terminal_runtimes = crate::terminal::TerminalRuntimeRegistry::new(); + let runtime = state + .runtime_for_pane(&terminal_runtimes, pane_id) + .expect("pane runtime after initial render"); + runtime.test_process_pty_bytes(b"\x1b[?2026h\x1b[2;3H"); + assert!(runtime.synchronized_output_active()); + + let resized_area = Rect::new(0, 0, 100, 30); + let (_buffer, cursor) = + crate::server::render_stream::render_virtual(&mut state, resized_area, true); + + assert_eq!( + cursor, None, + "pre-resize synchronized output should suppress the cursor even if resize clears the mode" + ); + } + + #[tokio::test] async fn virtual_render_exposes_hidden_pane_cursor_when_reveal_hidden_for_cjk_ime() { let mut state = AppState::test_new(); state.reveal_hidden_cursor_for_cjk_ime = true; diff --git a/src/server/render_stream.rs b/src/server/render_stream.rs index e158a6d4..6699298d 100644 --- a/src/server/render_stream.rs +++ b/src/server/render_stream.rs @@ -260,11 +260,15 @@ pub(crate) fn render_virtual_with_runtime_registry( resize_panes: bool, cell_size: crate::kitty_graphics::HostCellSize, ) -> (ratatui::buffer::Buffer, Option<CursorState>) { + let pre_compute_suppresses_focused_terminal_cursor = + focused_terminal_suppresses_host_cursor(app_state, terminal_runtimes); if resize_panes { crate::ui::compute_view_with_cell_size(app_state, terminal_runtimes, area, cell_size); } else { crate::ui::compute_view_without_resizing_panes(app_state, terminal_runtimes, area); } + let suppress_focused_terminal_cursor = pre_compute_suppresses_focused_terminal_cursor + || focused_terminal_suppresses_host_cursor(app_state, terminal_runtimes); let backend = CursorTrackingBackend::new(area.width, area.height); let mut terminal = ratatui::Terminal::new(backend).expect("TestBackend::new should never fail"); @@ -276,8 +280,15 @@ pub(crate) fn render_virtual_with_runtime_registry( .expect("render to TestBackend should never fail"); let buffer = terminal.backend().buffer().clone(); - let cursor = focused_terminal_cursor(app_state, terminal_runtimes) - .or_else(|| terminal.backend().rendered_cursor()); + let cursor = if suppress_focused_terminal_cursor { + None + } else { + focused_terminal_cursor(app_state, terminal_runtimes).or_else(|| { + (!focused_terminal_owns_host_cursor(app_state, terminal_runtimes)) + .then(|| terminal.backend().rendered_cursor()) + .flatten() + }) + }; (buffer, cursor) } @@ -287,6 +298,7 @@ pub(crate) fn render_terminal_virtual( runtime: &crate::terminal::TerminalRuntime, area: Rect, ) -> (ratatui::buffer::Buffer, Option<CursorState>) { + let suppress_cursor = runtime.synchronized_output_active(); let backend = CursorTrackingBackend::new(area.width, area.height); let mut terminal = ratatui::Terminal::new(backend).expect("TestBackend::new should never fail"); @@ -297,15 +309,20 @@ pub(crate) fn render_terminal_virtual( .expect("render to TestBackend should never fail"); let buffer = terminal.backend().buffer().clone(); - let cursor = runtime - .cursor_state(area, true) + let cursor = (!suppress_cursor) + .then(|| runtime.cursor_state(area, true)) + .flatten() .map(|cursor| CursorState { x: cursor.x, y: cursor.y, visible: cursor.visible && !crate::ui::pane_is_scrolled_back(runtime), shape: cursor.shape, }) - .or_else(|| terminal.backend().rendered_cursor()); + .or_else(|| { + (!suppress_cursor) + .then(|| terminal.backend().rendered_cursor()) + .flatten() + }); (buffer, cursor) } @@ -355,8 +372,10 @@ pub(crate) fn focused_terminal_cursor( return None; } let rt = app_state.runtime_for_pane_in_workspace(terminal_runtimes, ws_idx, info.id)?; + if rt.synchronized_output_active() { + return None; + } let scrolled_back = crate::ui::pane_is_scrolled_back(rt); - // Determine whether the IME-anchor reveal applies to this focused pane. // The master switch must be on, and either no agent filter is configured // (apply to any pane) or the focused pane's detected agent matches the @@ -406,3 +425,59 @@ pub(crate) fn focused_terminal_cursor( None } } + +fn focused_terminal_owns_host_cursor( + app_state: &AppState, + terminal_runtimes: &TerminalRuntimeRegistry, +) -> bool { + if app_state.mode != Mode::Terminal { + return false; + } + + let Some(ws_idx) = app_state.active else { + return false; + }; + let Some(info) = app_state + .view + .pane_infos + .iter() + .find(|info| info.is_focused) + else { + return false; + }; + if !app_state.pane_exposes_host_cursor(ws_idx, info.id) { + return false; + } + + app_state + .runtime_for_pane_in_workspace(terminal_runtimes, ws_idx, info.id) + .is_some() +} + +fn focused_terminal_suppresses_host_cursor( + app_state: &AppState, + terminal_runtimes: &TerminalRuntimeRegistry, +) -> bool { + if app_state.mode != Mode::Terminal { + return false; + } + + let Some(ws_idx) = app_state.active else { + return false; + }; + let Some(info) = app_state + .view + .pane_infos + .iter() + .find(|info| info.is_focused) + else { + return false; + }; + if !app_state.pane_exposes_host_cursor(ws_idx, info.id) { + return false; + } + + app_state + .runtime_for_pane_in_workspace(terminal_runtimes, ws_idx, info.id) + .is_some_and(crate::terminal::TerminalRuntime::synchronized_output_active) +} |
