aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-15 20:53:34 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-15 20:53:55 +0300
commit73dba97521f5a0e3104518ef48b775a178998f52 (patch)
treead9f95b4836303a35974b9ccdde229a1b05b03f4
parent4cbe72ec2d8a174e3c4f9bde2101aa189c7f38e7 (diff)
fix: forward pane cursor shape
fixes #116
-rw-r--r--src/api/schema.rs2
-rw-r--r--src/client/blit.rs122
-rw-r--r--src/ghostty/mod.rs43
-rw-r--r--src/pane.rs1
-rw-r--r--src/pane/terminal.rs54
-rw-r--r--src/server/headless.rs2
-rw-r--r--src/server/protocol.rs15
-rw-r--r--src/server/render_stream.rs2
-rw-r--r--tests/api_ping.rs2
-rw-r--r--tests/cli_wrapper.rs6
-rw-r--r--tests/client_mode.rs39
-rw-r--r--tests/cross_area.rs19
-rw-r--r--tests/detach_reattach.rs44
-rw-r--r--tests/multi_client.rs5
-rw-r--r--tests/server_headless.rs16
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.