aboutsummaryrefslogtreecommitdiff
path: root/src/server/headless.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/server/headless.rs')
-rw-r--r--src/server/headless.rs143
1 files changed, 143 insertions, 0 deletions
diff --git a/src/server/headless.rs b/src/server/headless.rs
index 49724ffe..f56dbdeb 100644
--- a/src/server/headless.rs
+++ b/src/server/headless.rs
@@ -2685,6 +2685,149 @@ mod tests {
}
#[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;
+ let mut ws = crate::workspace::Workspace::test_new("test");
+ let pane_id = ws.tabs[0].root_pane;
+ ws.insert_test_runtime(
+ pane_id,
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(20, 5, b"left\x1b[?25l"),
+ );
+
+ 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 (_buffer, cursor) =
+ crate::server::render_stream::render_virtual(&mut state, area, true);
+ let pane = state
+ .view
+ .pane_infos
+ .iter()
+ .find(|info| info.id == pane_id)
+ .expect("focused pane info");
+
+ assert_eq!(
+ cursor,
+ Some(CursorState {
+ x: pane.inner_rect.x + 4,
+ y: pane.inner_rect.y,
+ visible: true,
+ shape: state.cjk_ime_cursor_shape,
+ })
+ );
+ }
+
+ #[tokio::test]
+ async fn virtual_render_keeps_cursor_hidden_when_scrolled_back_even_with_reveal_hidden_for_cjk_ime(
+ ) {
+ 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 mut bytes = Vec::new();
+ for line in 0..80 {
+ bytes.extend_from_slice(format!("line {line:02}\r\n").as_bytes());
+ }
+ let runtime =
+ crate::terminal::TerminalRuntime::test_with_scrollback_bytes(20, 5, 4096, &bytes);
+ 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 runtime = state
+ .runtime_for_pane(pane_id)
+ .expect("pane runtime after initial render");
+ runtime.scroll_up(6);
+ assert!(crate::ui::pane_is_scrolled_back(runtime));
+
+ let (_buffer, cursor) =
+ crate::server::render_stream::render_virtual(&mut state, area, true);
+
+ assert!(
+ cursor.as_ref().is_none_or(|cursor| !cursor.visible),
+ "scrolled-back focused pane should keep the cursor hidden even when reveal_hidden_cursor_for_cjk_ime is true; got {cursor:?}",
+ );
+ }
+
+ #[tokio::test]
+ async fn virtual_render_fallback_cursor_when_viewport_none_and_reveal_hidden_for_cjk_ime() {
+ 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;
+ // Feed only ?25l with no prior cursor movement — exercises the fallback
+ // path for TUIs whose viewport has no cursor position.
+ ws.insert_test_runtime(
+ pane_id,
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(20, 5, b"\x1b[?25l"),
+ );
+
+ 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 (_buffer, cursor) =
+ crate::server::render_stream::render_virtual(&mut state, area, true);
+ let pane = state
+ .view
+ .pane_infos
+ .iter()
+ .find(|info| info.id == pane_id)
+ .expect("focused pane info");
+
+ assert_eq!(
+ cursor,
+ Some(CursorState {
+ x: pane.inner_rect.x,
+ y: pane.inner_rect.y,
+ visible: true,
+ shape: state.cjk_ime_cursor_shape,
+ }),
+ "fallback should anchor at pane top-left with the configured shape",
+ );
+ }
+
+ #[tokio::test]
+ async fn virtual_render_skips_reveal_when_focused_pane_has_no_detected_agent() {
+ let mut state = AppState::test_new();
+ state.reveal_hidden_cursor_for_cjk_ime = true;
+ // Filter only Claude, but the test pane has no detected agent, so the
+ // reveal must not apply.
+ state.cjk_ime_agents = vec![crate::detect::Agent::Claude];
+ let mut ws = crate::workspace::Workspace::test_new("test");
+ let pane_id = ws.tabs[0].root_pane;
+ ws.insert_test_runtime(
+ pane_id,
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(20, 5, b"left\x1b[?25l"),
+ );
+
+ 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 (_buffer, cursor) =
+ crate::server::render_stream::render_virtual(&mut state, area, true);
+
+ assert!(
+ cursor.as_ref().is_none_or(|cursor| !cursor.visible),
+ "agent filter should suppress reveal when the focused pane's detected agent is not on the list; got {cursor:?}",
+ );
+ }
+
+ #[tokio::test]
async fn virtual_render_omits_focused_pane_cursor_while_mobile_switcher_open() {
let mut state = AppState::test_new();
let mut ws = crate::workspace::Workspace::test_new("test");