diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-15 20:53:34 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-15 20:53:55 +0300 |
| commit | 73dba97521f5a0e3104518ef48b775a178998f52 (patch) | |
| tree | ad9f95b4836303a35974b9ccdde229a1b05b03f4 | |
| parent | 4cbe72ec2d8a174e3c4f9bde2101aa189c7f38e7 (diff) | |
fix: forward pane cursor shape
fixes #116
| -rw-r--r-- | src/api/schema.rs | 2 | ||||
| -rw-r--r-- | src/client/blit.rs | 122 | ||||
| -rw-r--r-- | src/ghostty/mod.rs | 43 | ||||
| -rw-r--r-- | src/pane.rs | 1 | ||||
| -rw-r--r-- | src/pane/terminal.rs | 54 | ||||
| -rw-r--r-- | src/server/headless.rs | 2 | ||||
| -rw-r--r-- | src/server/protocol.rs | 15 | ||||
| -rw-r--r-- | src/server/render_stream.rs | 2 | ||||
| -rw-r--r-- | tests/api_ping.rs | 2 | ||||
| -rw-r--r-- | tests/cli_wrapper.rs | 6 | ||||
| -rw-r--r-- | tests/client_mode.rs | 39 | ||||
| -rw-r--r-- | tests/cross_area.rs | 19 | ||||
| -rw-r--r-- | tests/detach_reattach.rs | 44 | ||||
| -rw-r--r-- | tests/multi_client.rs | 5 | ||||
| -rw-r--r-- | tests/server_headless.rs | 16 |
15 files changed, 299 insertions, 73 deletions
diff --git a/src/api/schema.rs b/src/api/schema.rs index cfd681ca..d2bf3ff6 100644 --- a/src/api/schema.rs +++ b/src/api/schema.rs @@ -994,7 +994,7 @@ mod tests { id: "req_1".into(), result: ResponseResult::Pong { version: "0.1.2".into(), - protocol: 5, + protocol: 6, }, }; diff --git a/src/client/blit.rs b/src/client/blit.rs index 4a5c90c3..2196e1ff 100644 --- a/src/client/blit.rs +++ b/src/client/blit.rs @@ -18,6 +18,7 @@ //! - `CSI H` (CUP) — move cursor to (row, col) //! - `CSI m` (SGR) — set graphic rendition (colors, bold, etc.) //! - `CSI ? 2026 h/l` — begin/end synchronized output +//! - `CSI Ps SP q` — DECSCUSR cursor shape //! - `ESC ] 52 ; c ; <base64> BEL` — OSC 52 clipboard write //! //! The goal is minimal output: skip unchanged cells, batch adjacent changes, @@ -37,6 +38,7 @@ pub(crate) struct EncodedBlit { /// Whether this frame was encoded as a full redraw. pub(crate) full: bool, next_last_visible_cursor: Option<(u16, u16)>, + next_last_cursor_shape: u8, } /// Stateful encoder that diffs semantic frames into terminal ANSI bytes. @@ -44,6 +46,7 @@ pub(crate) struct EncodedBlit { pub(crate) struct BlitEncoder { last_frame: Option<FrameData>, last_visible_cursor: Option<(u16, u16)>, + last_cursor_shape: u8, } impl BlitEncoder { @@ -62,16 +65,25 @@ impl BlitEncoder { || prev.is_some_and(|p| p.width != frame.width || p.height != frame.height); let mut bytes = Vec::new(); let mut next_last_visible_cursor = self.last_visible_cursor; - blit_frame_to_with_cursor_memory(&mut bytes, frame, prev, &mut next_last_visible_cursor); + let mut next_last_cursor_shape = self.last_cursor_shape; + blit_frame_to_with_cursor_memory( + &mut bytes, + frame, + prev, + &mut next_last_visible_cursor, + &mut next_last_cursor_shape, + ); EncodedBlit { bytes, full, next_last_visible_cursor, + next_last_cursor_shape, } } pub(crate) fn commit(&mut self, frame: FrameData, encoded: EncodedBlit) { self.last_visible_cursor = encoded.next_last_visible_cursor; + self.last_cursor_shape = encoded.next_last_cursor_shape; self.last_frame = Some(frame); } @@ -244,7 +256,14 @@ fn cells_equal(a: &CellData, b: &CellData) -> bool { #[cfg(test)] fn blit_frame_to(writer: impl Write, frame: &FrameData, prev: Option<&FrameData>) { let mut last_visible_cursor = None; - blit_frame_to_with_cursor_memory(writer, frame, prev, &mut last_visible_cursor); + let mut last_cursor_shape = 0; + blit_frame_to_with_cursor_memory( + writer, + frame, + prev, + &mut last_visible_cursor, + &mut last_cursor_shape, + ); } fn blit_frame_to_with_cursor_memory( @@ -252,6 +271,7 @@ fn blit_frame_to_with_cursor_memory( frame: &FrameData, prev: Option<&FrameData>, last_visible_cursor: &mut Option<(u16, u16)>, + last_cursor_shape: &mut u8, ) { // On first frame or size change, do a full redraw. let full_redraw = @@ -288,7 +308,7 @@ fn blit_frame_to_with_cursor_memory( // hides its cursor, still park the host cursor intentionally so IMEs do not // anchor to whichever cell happened to be painted last. let host_cursor = resolve_host_cursor_state(frame, last_visible_cursor); - write_host_cursor_state(&mut writer, host_cursor); + write_host_cursor_state(&mut writer, host_cursor, last_cursor_shape); // End the synchronized output block immediately after the final cursor // state is emitted so supporting terminals can present the frame atomically. @@ -312,6 +332,8 @@ fn cell_width(cell: &CellData) -> usize { struct HostCursorState { position: (u16, u16), visible: bool, + /// DECSCUSR parameter (0–6). 0 means terminal default. + shape: u8, } fn resolve_host_cursor_state( @@ -325,6 +347,7 @@ fn resolve_host_cursor_state( return HostCursorState { position, visible: true, + shape: normalize_cursor_shape(cursor.shape), }; } @@ -332,6 +355,7 @@ fn resolve_host_cursor_state( return HostCursorState { position, visible: false, + shape: normalize_cursor_shape(cursor.shape), }; } @@ -341,6 +365,15 @@ fn resolve_host_cursor_state( HostCursorState { position, visible: false, + shape: 0, + } +} + +fn normalize_cursor_shape(shape: u8) -> u8 { + if shape <= 6 { + shape + } else { + 0 } } @@ -363,8 +396,12 @@ fn write_cursor_position(writer: &mut impl Write, (x, y): (u16, u16)) { let _ = write!(writer, "\x1b[{};{}H", y + 1, x + 1); } -fn write_host_cursor_state(writer: &mut impl Write, cursor: HostCursorState) { +fn write_host_cursor_state(writer: &mut impl Write, cursor: HostCursorState, last_shape: &mut u8) { write_cursor_position(writer, cursor.position); + if cursor.shape != *last_shape { + let _ = write!(writer, "\x1b[{} q", cursor.shape); + *last_shape = cursor.shape; + } if cursor.visible { // Show cursor only after it is already at the final position. let _ = writer.write_all(b"\x1b[?25h"); @@ -780,6 +817,7 @@ mod tests { x: 2, y: 1, visible: true, + shape: 0, }), hyperlinks: Vec::new(), graphics: Vec::new(), @@ -800,6 +838,43 @@ mod tests { } #[test] + fn blit_frame_emits_cursor_shape_before_visibility_without_touching_ime_anchor() { + let frame = FrameData { + cells: vec![make_cell("A", 0, 0, 0)], + width: 1, + height: 1, + cursor: Some(CursorState { + x: 0, + y: 0, + visible: true, + shape: 6, + }), + hyperlinks: Vec::new(), + graphics: Vec::new(), + }; + + let mut output = Vec::new(); + blit_frame_to(&mut output, &frame, None); + + let output_str = String::from_utf8(output).unwrap(); + let final_cursor = output_str + .find("\x1b[1;1H\x1b[6 q\x1b[?25h") + .expect("should set cursor shape before showing cursor"); + let sync_end = output_str + .find("\x1b[?2026l") + .expect("should end synchronized output"); + assert!( + final_cursor < sync_end, + "shape should be part of the synchronized final cursor state" + ); + let trailing_cursor = &output_str[sync_end + "\x1b[?2026l".len()..]; + assert_eq!( + trailing_cursor, "\x1b[1;1H\x1b[?25h", + "IME anchor update should preserve the existing position/visibility-only contract" + ); + } + + #[test] fn blit_frame_repeats_explicit_hidden_cursor_anchor_after_synchronized_output() { let visible = FrameData { cells: vec![make_cell("A", 0, 0, 0); 9], @@ -809,6 +884,7 @@ mod tests { x: 0, y: 0, visible: true, + shape: 0, }), hyperlinks: Vec::new(), graphics: Vec::new(), @@ -821,20 +897,29 @@ mod tests { x: 2, y: 1, visible: false, + shape: 0, }), hyperlinks: Vec::new(), graphics: Vec::new(), }; let mut last_visible_cursor = None; + let mut last_cursor_shape = 0; let mut output = Vec::new(); - blit_frame_to_with_cursor_memory(&mut output, &visible, None, &mut last_visible_cursor); + blit_frame_to_with_cursor_memory( + &mut output, + &visible, + None, + &mut last_visible_cursor, + &mut last_cursor_shape, + ); output.clear(); blit_frame_to_with_cursor_memory( &mut output, &hidden, Some(&visible), &mut last_visible_cursor, + &mut last_cursor_shape, ); let output_str = String::from_utf8(output).unwrap(); @@ -976,6 +1061,7 @@ mod tests { x: 0, y: 0, visible: true, + shape: 0, }), hyperlinks: Vec::new(), graphics: Vec::new(), @@ -1001,6 +1087,7 @@ mod tests { x: 0, y: 0, visible: false, + shape: 0, }), hyperlinks: Vec::new(), graphics: Vec::new(), @@ -1048,6 +1135,7 @@ mod tests { x: 0, y: 0, visible: false, + shape: 0, }), hyperlinks: Vec::new(), graphics: Vec::new(), @@ -1069,6 +1157,7 @@ mod tests { x: 0, y: 0, visible: true, + shape: 0, }), hyperlinks: Vec::new(), graphics: Vec::new(), @@ -1097,6 +1186,7 @@ mod tests { x: 0, y: 0, visible: true, + shape: 0, }), hyperlinks: Vec::new(), graphics: Vec::new(), @@ -1107,6 +1197,7 @@ mod tests { x: 2, y: 2, visible: true, + shape: 0, }); let mut output = Vec::new(); @@ -1135,6 +1226,7 @@ mod tests { x: 1, y: 1, visible: true, + shape: 0, }), hyperlinks: Vec::new(), graphics: Vec::new(), @@ -1148,15 +1240,23 @@ mod tests { graphics: Vec::new(), }; let mut last_visible_cursor = None; + let mut last_cursor_shape = 0; let mut output = Vec::new(); - blit_frame_to_with_cursor_memory(&mut output, &visible, None, &mut last_visible_cursor); + blit_frame_to_with_cursor_memory( + &mut output, + &visible, + None, + &mut last_visible_cursor, + &mut last_cursor_shape, + ); output.clear(); blit_frame_to_with_cursor_memory( &mut output, &hidden, Some(&visible), &mut last_visible_cursor, + &mut last_cursor_shape, ); let output_str = String::from_utf8(output).unwrap(); @@ -1180,9 +1280,16 @@ mod tests { graphics: Vec::new(), }; let mut last_visible_cursor = None; + let mut last_cursor_shape = 0; let mut output = Vec::new(); - blit_frame_to_with_cursor_memory(&mut output, &frame, None, &mut last_visible_cursor); + blit_frame_to_with_cursor_memory( + &mut output, + &frame, + None, + &mut last_visible_cursor, + &mut last_cursor_shape, + ); let output_str = String::from_utf8(output).unwrap(); assert!( @@ -1201,6 +1308,7 @@ mod tests { x: 0, y: 0, visible: true, + shape: 0, }), hyperlinks: Vec::new(), graphics: Vec::new(), diff --git a/src/ghostty/mod.rs b/src/ghostty/mod.rs index a0ff3226..ef03716a 100644 --- a/src/ghostty/mod.rs +++ b/src/ghostty/mod.rs @@ -204,6 +204,31 @@ pub struct KittyPlacementRenderInfo { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CursorVisualStyle { + Bar, + Block, + Underline, + BlockHollow, +} + +impl CursorVisualStyle { + fn from_raw(value: ffi::GhosttyRenderStateCursorVisualStyle) -> Self { + match value { + ffi::GhosttyRenderStateCursorVisualStyle_GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK => { + Self::Block + } + ffi::GhosttyRenderStateCursorVisualStyle_GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_UNDERLINE => { + Self::Underline + } + ffi::GhosttyRenderStateCursorVisualStyle_GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BLOCK_HOLLOW => { + Self::BlockHollow + } + _ => Self::Bar, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ActiveScreen { Primary, Alternate, @@ -1278,6 +1303,24 @@ impl RenderState { self.get_bool(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE) } + pub fn cursor_blinking(&self) -> Result<bool, Error> { + self.get_bool(ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_BLINKING) + } + + pub fn cursor_visual_style(&self) -> Result<CursorVisualStyle, Error> { + let mut out: ffi::GhosttyRenderStateCursorVisualStyle = 0; + // SAFETY: out points to the matching enum storage for the requested data kind. + unsafe { + ffi::ghostty_render_state_get( + self.raw, + ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VISUAL_STYLE, + (&mut out as *mut ffi::GhosttyRenderStateCursorVisualStyle).cast(), + ) + .into_result()?; + } + Ok(CursorVisualStyle::from_raw(out)) + } + pub fn cursor_viewport(&self) -> Result<Option<CursorViewport>, Error> { if !self.get_bool( ffi::GhosttyRenderStateData_GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE, diff --git a/src/pane.rs b/src/pane.rs index d9b63fb9..522365df 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -790,6 +790,7 @@ impl PaneRuntime { x: area.x + cursor.x, y: area.y + cursor.y, visible: cursor.visible, + shape: cursor.shape, }) } diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index 33cd8ed5..f0535c7a 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -39,6 +39,21 @@ pub struct TerminalCursorState { pub x: u16, pub y: u16, pub visible: bool, + /// DECSCUSR parameter (0–6). 0 means terminal default. + pub shape: u8, +} + +fn decscusr_cursor_shape(style: crate::ghostty::CursorVisualStyle, blinking: bool) -> u8 { + match (style, blinking) { + (crate::ghostty::CursorVisualStyle::Block, true) + | (crate::ghostty::CursorVisualStyle::BlockHollow, true) => 1, + (crate::ghostty::CursorVisualStyle::Block, false) + | (crate::ghostty::CursorVisualStyle::BlockHollow, false) => 2, + (crate::ghostty::CursorVisualStyle::Underline, true) => 3, + (crate::ghostty::CursorVisualStyle::Underline, false) => 4, + (crate::ghostty::CursorVisualStyle::Bar, true) => 5, + (crate::ghostty::CursorVisualStyle::Bar, false) => 6, + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -530,10 +545,17 @@ impl GhosttyPaneTerminal { } = &mut *core; render_state.update(terminal).ok()?; let cursor = render_state.cursor_viewport().ok()??; + let shape = render_state + .cursor_visual_style() + .ok() + .zip(render_state.cursor_blinking().ok()) + .map(|(style, blinking)| decscusr_cursor_shape(style, blinking)) + .unwrap_or(0); Some(TerminalCursorState { x: cursor.x, y: cursor.y, visible: render_state.cursor_visible().ok()?, + shape, }) } @@ -1203,6 +1225,38 @@ mod tests { } #[test] + fn decscusr_cursor_shape_preserves_blinking_variants() { + assert_eq!( + decscusr_cursor_shape(crate::ghostty::CursorVisualStyle::Block, true), + 1 + ); + assert_eq!( + decscusr_cursor_shape(crate::ghostty::CursorVisualStyle::Block, false), + 2 + ); + assert_eq!( + decscusr_cursor_shape(crate::ghostty::CursorVisualStyle::Underline, true), + 3 + ); + assert_eq!( + decscusr_cursor_shape(crate::ghostty::CursorVisualStyle::Underline, false), + 4 + ); + assert_eq!( + decscusr_cursor_shape(crate::ghostty::CursorVisualStyle::Bar, true), + 5 + ); + assert_eq!( + decscusr_cursor_shape(crate::ghostty::CursorVisualStyle::Bar, false), + 6 + ); + assert_eq!( + decscusr_cursor_shape(crate::ghostty::CursorVisualStyle::BlockHollow, false), + 2 + ); + } + + #[test] fn host_terminal_theme_restore_probe_skips_when_no_transient_override() { let (tx, _rx) = mpsc::channel(4); let terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap(); diff --git a/src/server/headless.rs b/src/server/headless.rs index cc57c1f5..9bb3181f 100644 --- a/src/server/headless.rs +++ b/src/server/headless.rs @@ -2137,6 +2137,7 @@ mod tests { x: pane.inner_rect.x + 4, y: pane.inner_rect.y, visible: true, + shape: cursor.as_ref().map(|c| c.shape).unwrap_or(0), }) ); } @@ -2172,6 +2173,7 @@ mod tests { x: pane.inner_rect.x + 4, y: pane.inner_rect.y, visible: false, + shape: cursor.as_ref().map(|c| c.shape).unwrap_or(0), }) ); } diff --git a/src/server/protocol.rs b/src/server/protocol.rs index e9b7975e..7d0d3e2d 100644 --- a/src/server/protocol.rs +++ b/src/server/protocol.rs @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; // --------------------------------------------------------------------------- /// Current protocol version. Bumped when wire format changes incompatibly. -pub const PROTOCOL_VERSION: u32 = 5; +pub const PROTOCOL_VERSION: u32 = 6; /// Maximum allowed frame payload size (2 MB). Frames larger than this are /// rejected to prevent denial-of-service via oversized length prefixes. @@ -103,6 +103,13 @@ pub struct CellData { pub hyperlink: Option<u32>, } +/// Cursor shape encoded as a DECSCUSR parameter. +/// +/// 0 = terminal default, 1 = blinking block, 2 = steady block, +/// 3 = blinking underline, 4 = steady underline, 5 = blinking bar, +/// 6 = steady bar. +pub type CursorShapeParam = u8; + /// Cursor position within a rendered frame. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CursorState { @@ -112,6 +119,9 @@ pub struct CursorState { pub y: u16, /// Whether the cursor is visible. pub visible: bool, + /// Cursor shape as a DECSCUSR parameter. + #[serde(default)] + pub shape: CursorShapeParam, } /// A rendered frame to be displayed by the client. @@ -725,6 +735,7 @@ mod tests { x: 0, y: 0, visible: true, + shape: 6, }), hyperlinks: vec!["https://example.com".to_owned()], graphics: Vec::new(), @@ -875,6 +886,7 @@ mod tests { x: 10, y: 5, visible: true, + shape: 0, }), hyperlinks: Vec::new(), graphics: Vec::new(), @@ -1151,6 +1163,7 @@ mod tests { x: 1, y: 0, visible: true, + shape: 0, }; let frame = FrameData::from_ratatui_buffer(&buffer, Some(cursor.clone())); diff --git a/src/server/render_stream.rs b/src/server/render_stream.rs index ca0ecddd..a921960c 100644 --- a/src/server/render_stream.rs +++ b/src/server/render_stream.rs @@ -151,6 +151,7 @@ impl CursorTrackingBackend { x: pos.x, y: pos.y, visible: true, + shape: 0, }) } } @@ -296,5 +297,6 @@ fn focused_terminal_cursor(app_state: &AppState) -> Option<CursorState> { x: cursor.x, y: cursor.y, visible: cursor.visible && !crate::ui::pane_is_scrolled_back(rt), + shape: cursor.shape, }) } diff --git a/tests/api_ping.rs b/tests/api_ping.rs index 8abaf267..30840e91 100644 --- a/tests/api_ping.rs +++ b/tests/api_ping.rs @@ -227,7 +227,7 @@ fn ping_over_socket_returns_version() { assert_eq!(value["result"]["version"], env!("CARGO_PKG_VERSION")); // Intentionally hardcoded so wire protocol bumps require updating this test. // Changing this value means old clients/servers are no longer compatible. - assert_eq!(value["result"]["protocol"], 5); + assert_eq!(value["result"]["protocol"], 6); cleanup_spawned_herdr(child, base); } diff --git a/tests/cli_wrapper.rs b/tests/cli_wrapper.rs index 357482e9..52e99edb 100644 --- a/tests/cli_wrapper.rs +++ b/tests/cli_wrapper.rs @@ -938,7 +938,7 @@ fn status_commands_report_client_and_server_versions() { "stdout: {full_stdout}" ); assert!( - full_stdout.contains(" protocol: 5"), + full_stdout.contains(" protocol: 6"), "stdout: {full_stdout}" ); assert!(full_stdout.contains("server:\n"), "stdout: {full_stdout}"); @@ -971,7 +971,7 @@ fn status_commands_report_client_and_server_versions() { "stdout: {server_stdout}" ); assert!( - server_stdout.contains("protocol: 5"), + server_stdout.contains("protocol: 6"), "stdout: {server_stdout}" ); @@ -983,7 +983,7 @@ fn status_commands_report_client_and_server_versions() { "stdout: {client_stdout}" ); assert!( - client_stdout.contains("protocol: 5"), + client_stdout.contains("protocol: 6"), "stdout: {client_stdout}" ); assert!( diff --git a/tests/client_mode.rs b/tests/client_mode.rs index 150b8733..79dfaae3 100644 --- a/tests/client_mode.rs +++ b/tests/client_mode.rs @@ -186,6 +186,7 @@ struct CursorWire { x: u16, y: u16, visible: bool, + shape: u8, } fn decode_frame_payload(payload: &[u8]) -> std::io::Result<FrameWire> { @@ -237,7 +238,7 @@ fn frame_contains_text(frame: &FrameWire, needle: &str) -> bool { } let _ = (frame.height, frame.graphics.len()); if let Some(cursor) = frame.cursor.as_ref() { - let _ = (cursor.x, cursor.y, cursor.visible); + let _ = (cursor.x, cursor.y, cursor.visible, cursor.shape); } text.contains(needle) @@ -266,8 +267,8 @@ fn client_connects_and_receives_frame() { // Connect and handshake. let mut stream = UnixStream::connect(&client_socket).expect("should connect to client socket"); let (version, error) = - client_handshake(&mut stream, 5, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 5, "server should report protocol version 5"); + client_handshake(&mut stream, 6, 80, 24).expect("handshake should succeed"); + assert_eq!(version, 6, "server should report protocol version 6"); assert!( error.is_none(), "handshake should not have error: {:?}", @@ -334,8 +335,8 @@ fn client_sees_headless_startup_config_diagnostic() { let mut stream = UnixStream::connect(&client_socket).expect("should connect to client socket"); let (version, error) = - client_handshake(&mut stream, 5, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 5); + client_handshake(&mut stream, 6, 80, 24).expect("handshake should succeed"); + assert_eq!(version, 6); assert!(error.is_none(), "{:?}", error); stream @@ -383,8 +384,8 @@ fn client_input_forwarded_to_pane() { // Connect and handshake. let mut stream = UnixStream::connect(&client_socket).expect("should connect to client socket"); let (version, error) = - client_handshake(&mut stream, 5, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 5); + client_handshake(&mut stream, 6, 80, 24).expect("handshake should succeed"); + assert_eq!(version, 6); assert!(error.is_none(), "{:?}", error); // Send an Input message containing "echo hello\n". @@ -437,8 +438,8 @@ fn client_resize_sends_message() { // Connect and handshake. let mut stream = UnixStream::connect(&client_socket).expect("should connect to client socket"); let (version, error) = - client_handshake(&mut stream, 5, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 5); + client_handshake(&mut stream, 6, 80, 24).expect("handshake should succeed"); + assert_eq!(version, 6); assert!(error.is_none(), "{:?}", error); // Drain the initial frame(s). @@ -496,8 +497,8 @@ fn server_shutdown_sends_message_to_client() { // Connect and handshake. let mut stream = UnixStream::connect(&client_socket).expect("should connect to client socket"); let (version, error) = - client_handshake(&mut stream, 5, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 5); + client_handshake(&mut stream, 6, 80, 24).expect("handshake should succeed"); + assert_eq!(version, 6); assert!(error.is_none(), "{:?}", error); // Send SIGINT so the server takes the graceful shutdown path and @@ -724,8 +725,8 @@ fn client_receives_frame_after_pane_output() { // Connect and handshake. let mut stream = UnixStream::connect(&client_socket).expect("should connect to client socket"); let (version, error) = - client_handshake(&mut stream, 5, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 5); + client_handshake(&mut stream, 6, 80, 24).expect("handshake should succeed"); + assert_eq!(version, 6); assert!(error.is_none(), "{:?}", error); read_next_frame_payload(&mut stream, Duration::from_secs(10)) @@ -771,8 +772,8 @@ fn navigate_mode_keybind_dispatch_in_server() { // Connect and handshake. let mut stream = UnixStream::connect(&client_socket).expect("should connect to client socket"); let (version, error) = - client_handshake(&mut stream, 5, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 5); + client_handshake(&mut stream, 6, 80, 24).expect("handshake should succeed"); + assert_eq!(version, 6); assert!(error.is_none(), "{:?}", error); // Drain initial frames. @@ -889,8 +890,8 @@ fn graceful_shutdown_sends_server_shutdown_to_client() { // Connect and handshake. let mut stream = UnixStream::connect(&client_socket).expect("should connect to client socket"); let (version, error) = - client_handshake(&mut stream, 5, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 5); + client_handshake(&mut stream, 6, 80, 24).expect("handshake should succeed"); + assert_eq!(version, 6); assert!(error.is_none(), "{:?}", error); // Drain initial frame(s). @@ -987,8 +988,8 @@ fn client_receives_notify_on_agent_state_change() { // Connect as a client and perform handshake. let mut stream = UnixStream::connect(&client_socket).expect("should connect"); let (version, error) = - client_handshake(&mut stream, 5, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 5); + client_handshake(&mut stream, 6, 80, 24).expect("handshake should succeed"); + assert_eq!(version, 6); assert!(error.is_none(), "{:?}", error); // Drain initial frame(s). diff --git a/tests/cross_area.rs b/tests/cross_area.rs index 963214b3..b5910e92 100644 --- a/tests/cross_area.rs +++ b/tests/cross_area.rs @@ -518,6 +518,7 @@ struct CursorWire { x: u16, y: u16, visible: bool, + shape: u8, } fn decode_frame_payload(payload: &[u8]) -> io::Result<FrameWire> { @@ -554,7 +555,7 @@ fn frame_contains_text(frame: &FrameWire, needle: &str) -> bool { } let _ = (frame.height, frame.graphics.len()); if let Some(cursor) = frame.cursor.as_ref() { - let _ = (cursor.x, cursor.y, cursor.visible); + let _ = (cursor.x, cursor.y, cursor.visible, cursor.shape); } text.contains(needle) @@ -677,7 +678,7 @@ fn cross_area_detach_and_reattach_preserves_state() { // Local attach (client A). let mut client_a = UnixStream::connect(&client_socket).expect("client A should connect"); - client_handshake(&mut client_a, 5, 100, 30); + client_handshake(&mut client_a, 6, 100, 30); assert!(wait_for_frame(&mut client_a, Duration::from_secs(2))); // Use herdr: create a workspace and write output into its pane. @@ -714,7 +715,7 @@ fn cross_area_detach_and_reattach_preserves_state() { // Reattach from another terminal/session (client B). let mut client_b = UnixStream::connect(&client_socket).expect("client B should connect"); - client_handshake(&mut client_b, 5, 80, 24); + client_handshake(&mut client_b, 6, 80, 24); assert!( wait_for_frame(&mut client_b, Duration::from_secs(5)), "reattached client should receive frame" @@ -770,7 +771,7 @@ fn cross_area_agent_process_survives_detach_and_reattach() { wait_for_socket(&client_socket, Duration::from_secs(10)); let mut client_a = UnixStream::connect(&client_socket).expect("client A should connect"); - client_handshake(&mut client_a, 5, 100, 30); + client_handshake(&mut client_a, 6, 100, 30); assert!(wait_for_frame(&mut client_a, Duration::from_secs(2))); let created = workspace_create(&api_socket, "agent-persist"); @@ -823,7 +824,7 @@ fn cross_area_agent_process_survives_detach_and_reattach() { // Reattach and ensure client-side state reflects the persisted working status. let mut client_b = UnixStream::connect(&client_socket).expect("client B should connect"); - client_handshake(&mut client_b, 5, 80, 24); + client_handshake(&mut client_b, 6, 80, 24); let saw_working_on_client = wait_for_frame_matching(&mut client_b, Duration::from_secs(5), |frame| { frame_contains_text(frame, "working") @@ -868,7 +869,7 @@ fn cross_area_client_and_api_workspace_views_are_consistent() { wait_for_socket(&client_socket, Duration::from_secs(10)); let mut client = UnixStream::connect(&client_socket).expect("client should connect"); - client_handshake(&mut client, 5, 100, 30); + client_handshake(&mut client, 6, 100, 30); assert!(wait_for_frame(&mut client, Duration::from_secs(2))); drain_server_messages(&mut client, Duration::from_millis(300)); @@ -931,9 +932,9 @@ fn cross_area_two_clients_shared_view_and_single_detach_stability() { wait_for_socket(&client_socket, Duration::from_secs(10)); let mut client_a = UnixStream::connect(&client_socket).expect("client A should connect"); - client_handshake(&mut client_a, 5, 110, 30); + client_handshake(&mut client_a, 6, 110, 30); let mut client_b = UnixStream::connect(&client_socket).expect("client B should connect"); - client_handshake(&mut client_b, 5, 100, 30); + client_handshake(&mut client_b, 6, 100, 30); assert!(wait_for_frame(&mut client_a, Duration::from_secs(2))); assert!(wait_for_frame(&mut client_b, Duration::from_secs(2))); @@ -1099,7 +1100,7 @@ fn cross_area_server_kill_then_restart_and_reconnect() { let mut reconnect_client = UnixStream::connect(&client_socket).expect("new client should connect after restart"); - client_handshake(&mut reconnect_client, 5, 80, 24); + client_handshake(&mut reconnect_client, 6, 80, 24); assert!( wait_for_frame(&mut reconnect_client, Duration::from_secs(5)), "new client should receive frame after restart" diff --git a/tests/detach_reattach.rs b/tests/detach_reattach.rs index 588d5713..67e634a7 100644 --- a/tests/detach_reattach.rs +++ b/tests/detach_reattach.rs @@ -276,8 +276,8 @@ fn navigate_q_detaches_client_and_server_persists() { // Connect and handshake. let mut stream = UnixStream::connect(&client_socket).expect("should connect to client socket"); let (version, error) = - client_handshake(&mut stream, 5, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 5); + client_handshake(&mut stream, 6, 80, 24).expect("handshake should succeed"); + assert_eq!(version, 6); assert!(error.is_none(), "{:?}", error); // Drain initial frames. @@ -338,8 +338,8 @@ fn explicit_detach_message_causes_clean_disconnect() { // Connect and handshake. let mut stream = UnixStream::connect(&client_socket).expect("should connect"); let (version, error) = - client_handshake(&mut stream, 5, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 5); + client_handshake(&mut stream, 6, 80, 24).expect("handshake should succeed"); + assert_eq!(version, 6); assert!(error.is_none(), "{:?}", error); // Drain initial frames. @@ -397,8 +397,8 @@ fn reattach_after_detach_shows_current_state() { // --- Client A --- let mut stream_a = UnixStream::connect(&client_socket).expect("client A should connect"); let (version, error) = - client_handshake(&mut stream_a, 5, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 5); + client_handshake(&mut stream_a, 6, 80, 24).expect("handshake should succeed"); + assert_eq!(version, 6); assert!(error.is_none(), "{:?}", error); // Drain initial frames. @@ -436,8 +436,8 @@ fn reattach_after_detach_shows_current_state() { // --- Client B (reattach) --- let mut stream_b = UnixStream::connect(&client_socket).expect("client B should connect"); let (version, error) = - client_handshake(&mut stream_b, 5, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 5); + client_handshake(&mut stream_b, 6, 80, 24).expect("handshake should succeed"); + assert_eq!(version, 6); assert!( error.is_none(), "reattach handshake should succeed: {:?}", @@ -516,8 +516,8 @@ fn processes_survive_during_and_after_detach() { // Connect and handshake. let mut stream = UnixStream::connect(&client_socket).expect("should connect"); let (version, error) = - client_handshake(&mut stream, 5, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 5); + client_handshake(&mut stream, 6, 80, 24).expect("handshake should succeed"); + assert_eq!(version, 6); assert!(error.is_none(), "{:?}", error); // Drain initial frames. @@ -555,8 +555,8 @@ fn processes_survive_during_and_after_detach() { // Reattach — verify we can connect and receive a frame. let mut stream_b = UnixStream::connect(&client_socket).expect("should reattach"); let (version, error) = - client_handshake(&mut stream_b, 5, 80, 24).expect("reattach handshake should succeed"); - assert_eq!(version, 5); + client_handshake(&mut stream_b, 6, 80, 24).expect("reattach handshake should succeed"); + assert_eq!(version, 6); assert!(error.is_none(), "{:?}", error); // Verify the reattached client receives a frame. @@ -604,8 +604,8 @@ fn server_persists_after_client_connection_drop() { // Connect and handshake. let mut stream = UnixStream::connect(&client_socket).expect("should connect"); let (version, error) = - client_handshake(&mut stream, 5, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 5); + client_handshake(&mut stream, 6, 80, 24).expect("handshake should succeed"); + assert_eq!(version, 6); assert!(error.is_none(), "{:?}", error); // Drain initial frames. @@ -631,8 +631,8 @@ fn server_persists_after_client_connection_drop() { // Reattach — verify we can connect and handshake again. let mut stream_b = UnixStream::connect(&client_socket).expect("should reattach"); let (version, error) = - client_handshake(&mut stream_b, 5, 80, 24).expect("reattach handshake should succeed"); - assert_eq!(version, 5); + client_handshake(&mut stream_b, 6, 80, 24).expect("reattach handshake should succeed"); + assert_eq!(version, 6); assert!(error.is_none(), "reattach should succeed: {:?}", error); cleanup_spawned_herdr(spawned, base); @@ -653,8 +653,8 @@ fn detached_output_preserves_last_attached_pty_size() { let mut stream = UnixStream::connect(&client_socket).expect("client should connect"); let (version, error) = - client_handshake(&mut stream, 5, 120, 40).expect("handshake should succeed"); - assert_eq!(version, 5); + client_handshake(&mut stream, 6, 120, 40).expect("handshake should succeed"); + assert_eq!(version, 6); assert!(error.is_none(), "{:?}", error); drain_messages(&mut stream); @@ -722,8 +722,8 @@ fn output_accumulated_while_detached_visible_on_reattach() { // Connect and handshake client A. let mut stream_a = UnixStream::connect(&client_socket).expect("client A should connect"); let (version, error) = - client_handshake(&mut stream_a, 5, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 5); + client_handshake(&mut stream_a, 6, 80, 24).expect("handshake should succeed"); + assert_eq!(version, 6); assert!(error.is_none(), "{:?}", error); // Detach client A immediately. @@ -780,8 +780,8 @@ fn output_accumulated_while_detached_visible_on_reattach() { // --- Client B (reattach) --- let mut stream_b = UnixStream::connect(&client_socket).expect("client B should connect"); let (version, error) = - client_handshake(&mut stream_b, 5, 80, 24).expect("reattach handshake should succeed"); - assert_eq!(version, 5); + client_handshake(&mut stream_b, 6, 80, 24).expect("reattach handshake should succeed"); + assert_eq!(version, 6); assert!(error.is_none(), "{:?}", error); // Client B should receive a frame with the current state. diff --git a/tests/multi_client.rs b/tests/multi_client.rs index 704b511e..103c3f1f 100644 --- a/tests/multi_client.rs +++ b/tests/multi_client.rs @@ -549,7 +549,7 @@ fn client_handshake( fn connect_raw_client(client_socket: &Path, cols: u16, rows: u16) -> UnixStream { let mut stream = UnixStream::connect(client_socket).expect("should connect to client socket"); - client_handshake(&mut stream, 5, cols, rows).expect("handshake should succeed"); + client_handshake(&mut stream, 6, cols, rows).expect("handshake should succeed"); stream } @@ -599,6 +599,7 @@ struct CursorWire { x: u16, y: u16, visible: bool, + shape: u8, } fn decode_frame_payload(payload: &[u8]) -> io::Result<FrameWire> { @@ -714,7 +715,7 @@ fn frame_contains_text(frame: &FrameWire, needle: &str) -> bool { let _ = (frame.height, frame.graphics.len()); if let Some(cursor) = frame.cursor.as_ref() { - let _ = (cursor.x, cursor.y, cursor.visible); + let _ = (cursor.x, cursor.y, cursor.visible, cursor.shape); } full_text.contains(needle) diff --git a/tests/server_headless.rs b/tests/server_headless.rs index 209a8045..51a9d005 100644 --- a/tests/server_headless.rs +++ b/tests/server_headless.rs @@ -583,11 +583,11 @@ fn client_handshake_succeeds() { // Connect to the client socket and perform a handshake. let mut stream = UnixStream::connect(&client_socket).expect("should connect to client socket"); - // Send Hello with version 5, 80 cols, 24 rows. + // Send Hello with version 6, 80 cols, 24 rows. let (version, error) = - client_handshake(&mut stream, 5, 80, 24).expect("handshake should succeed"); + client_handshake(&mut stream, 6, 80, 24).expect("handshake should succeed"); - assert_eq!(version, 5, "server should report protocol version 5"); + assert_eq!(version, 6, "server should report protocol version 6"); assert!( error.is_none(), "handshake should not have an error: {:?}", @@ -616,7 +616,7 @@ fn client_handshake_rejects_incompatible_version() { let (version, error) = client_handshake(&mut stream, 0, 80, 24) .expect("should read Welcome response even on rejection"); - assert_eq!(version, 5, "server should report its version 5"); + assert_eq!(version, 6, "server should report its version 6"); assert!( error.is_some(), "version 0 should be rejected with an error" @@ -641,10 +641,10 @@ fn client_handshake_clamps_small_terminal_size() { // Send Hello with 0x0 terminal size — should be clamped. let mut stream = UnixStream::connect(&client_socket).expect("should connect to client socket"); - let (version, error) = client_handshake(&mut stream, 5, 0, 0) + let (version, error) = client_handshake(&mut stream, 6, 0, 0) .expect("handshake with 0x0 should succeed (server clamps)"); - assert_eq!(version, 5); + assert_eq!(version, 6); assert!( error.is_none(), "0x0 size should be accepted (clamped): {:?}", @@ -704,9 +704,9 @@ fn no_hello_client_closed_within_five_seconds() { // Verify the server is still healthy — a proper client can still connect. let mut good_stream = UnixStream::connect(&client_socket).expect("should connect after no-hello client"); - let (version, error) = client_handshake(&mut good_stream, 5, 80, 24) + let (version, error) = client_handshake(&mut good_stream, 6, 80, 24) .expect("proper handshake should still work after no-hello client"); - assert_eq!(version, 5); + assert_eq!(version, 6); assert!(error.is_none()); // API should still work. |
