aboutsummaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-08 01:36:51 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-08 01:36:51 +0300
commitfb56e64537de5f93786ed121f703da2a4105badc (patch)
tree67f6e100141dbee05c75ec421927d62e54d23ec6 /src/server
parent416827872a6e2c820bd58209d267685d4c07cb7d (diff)
fix: improve windows pane behavior
Diffstat (limited to 'src/server')
-rw-r--r--src/server/headless.rs64
-rw-r--r--src/server/render_stream.rs87
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)
+}