aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-02 21:57:59 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-02 21:59:06 +0300
commitd1471e64834d0c7daf072f11c01c30db35324850 (patch)
tree5763e6da098d3aabe5932decf26e5cf1d31c814c /src/client
parent848554517ae6473caf61f738d60def90f77e1dc9 (diff)
fix: draw host cursor on windows
refs #930
Diffstat (limited to 'src/client')
-rw-r--r--src/client/mod.rs63
1 files changed, 60 insertions, 3 deletions
diff --git a/src/client/mod.rs b/src/client/mod.rs
index 4e0a54ec..92edb900 100644
--- a/src/client/mod.rs
+++ b/src/client/mod.rs
@@ -55,6 +55,7 @@ struct ClientLoopConfig {
sound_config: crate::config::SoundConfig,
mouse_scroll_lines: usize,
redraw_on_focus_gained: bool,
+ host_cursor: crate::config::HostCursorModeConfig,
kitty_graphics_enabled: bool,
mouse_capture_active: bool,
#[cfg(unix)]
@@ -83,6 +84,8 @@ struct ClientState {
remote_image_paste_key: Option<(crossterm::event::KeyCode, crossterm::event::KeyModifiers)>,
/// Whether outer focus gain should force a full host-terminal redraw.
redraw_on_focus_gained: bool,
+ /// Whether this client draws the cursor into frame cells instead of using the host cursor.
+ draw_host_cursor: bool,
}
#[derive(Debug, Default)]
@@ -440,6 +443,14 @@ fn write_terminal_restore_postlude(
writer.flush()
}
+fn should_draw_host_cursor(mode: crate::config::HostCursorModeConfig) -> bool {
+ match mode {
+ crate::config::HostCursorModeConfig::Auto => cfg!(windows),
+ crate::config::HostCursorModeConfig::Native => false,
+ crate::config::HostCursorModeConfig::Drawn => true,
+ }
+}
+
#[cfg(windows)]
#[derive(Default)]
struct WindowsVirtualTerminalInputSetup {
@@ -1092,6 +1103,7 @@ fn run_client_with_mode(
let mouse_capture = loaded_config.config.ui.mouse_capture;
let mouse_scroll_lines = loaded_config.config.ui.mouse_scroll_lines();
let redraw_on_focus_gained = loaded_config.config.ui.redraw_on_focus_gained;
+ let host_cursor = loaded_config.config.ui.host_cursor;
let direct_attach_requested = attach_request.is_some();
#[cfg(unix)]
let remote_image_paste_key = client_remote_image_paste_key(&loaded_config.config);
@@ -1101,6 +1113,7 @@ fn run_client_with_mode(
sound_config: loaded_config.config.ui.sound,
mouse_scroll_lines,
redraw_on_focus_gained,
+ host_cursor,
kitty_graphics_enabled,
mouse_capture_active: mouse_capture,
#[cfg(unix)]
@@ -1253,6 +1266,7 @@ async fn run_client_loop(
) -> Result<(), ClientError> {
#[cfg(windows)]
let _ = config.mouse_scroll_lines;
+ let draw_host_cursor = attach_escape.is_none() && should_draw_host_cursor(config.host_cursor);
let mut state = ClientState {
blit_encoder: render_ansi::BlitEncoder::new(),
@@ -1266,6 +1280,7 @@ async fn run_client_loop(
#[cfg(unix)]
remote_image_paste_key: config.remote_image_paste_key,
redraw_on_focus_gained: config.redraw_on_focus_gained,
+ draw_host_cursor,
};
debug!(?negotiated_encoding, "client render encoding active");
@@ -1443,7 +1458,18 @@ async fn run_client_loop(
}
ClientLoopEvent::ServerMessage(msg) => match msg {
ServerMessage::Frame(frame_data) => {
- let encoded = state.blit_encoder.encode(&frame_data, false);
+ let frame_data = if state.draw_host_cursor {
+ render_ansi::frame_with_drawn_cursor(frame_data)
+ } else {
+ frame_data
+ };
+ let encoded = if state.draw_host_cursor {
+ state
+ .blit_encoder
+ .encode_with_suppressed_visible_cursor(&frame_data, false)
+ } else {
+ state.blit_encoder.encode(&frame_data, false)
+ };
let mut stdout = io::stdout();
let graphics = if state.kitty_graphics_enabled {
frame_data.graphics.as_slice()
@@ -1493,6 +1519,7 @@ async fn run_client_loop(
reload_local_client_config(
&mut state.sound_config,
&mut state.redraw_on_focus_gained,
+ &mut state.draw_host_cursor,
#[cfg(unix)]
&mut state.remote_image_paste_key,
);
@@ -1618,6 +1645,7 @@ fn client_remote_image_paste_key(
fn reload_local_client_config(
sound_config: &mut crate::config::SoundConfig,
redraw_on_focus_gained: &mut bool,
+ draw_host_cursor: &mut bool,
#[cfg(unix)] remote_image_paste_key: &mut Option<(
crossterm::event::KeyCode,
crossterm::event::KeyModifiers,
@@ -1632,6 +1660,7 @@ fn reload_local_client_config(
let loaded_remote_image_paste_key = client_remote_image_paste_key(&loaded.config);
*sound_config = loaded.config.ui.sound;
*redraw_on_focus_gained = loaded.config.ui.redraw_on_focus_gained;
+ *draw_host_cursor = should_draw_host_cursor(loaded.config.ui.host_cursor);
#[cfg(unix)]
{
*remote_image_paste_key = loaded_remote_image_paste_key;
@@ -2007,6 +2036,27 @@ mod tests {
}
}
+ #[test]
+ fn host_cursor_policy_auto_uses_drawn_cursor_on_windows() {
+ assert_eq!(
+ should_draw_host_cursor(crate::config::HostCursorModeConfig::Auto),
+ cfg!(windows)
+ );
+ }
+
+ #[test]
+ fn host_cursor_policy_native_and_drawn_override_auto_detection() {
+ let _guard = env_lock().lock().unwrap();
+ let _env = EnvVarGuard::set("TERM_PROGRAM", "WezTerm");
+
+ assert!(!should_draw_host_cursor(
+ crate::config::HostCursorModeConfig::Native
+ ));
+ assert!(should_draw_host_cursor(
+ crate::config::HostCursorModeConfig::Drawn
+ ));
+ }
+
#[cfg(unix)]
#[test]
fn clipboard_image_paste_bridge_triggers_on_configured_key_and_empty_paste() {
@@ -2436,7 +2486,7 @@ mod tests {
}
#[test]
- fn reload_local_client_config_refreshes_redraw_on_focus_gained() {
+ fn reload_local_client_config_refreshes_local_client_presentation_state() {
let _guard = crate::config::test_config_env_lock().lock().unwrap();
let path = std::env::temp_dir().join(format!(
"herdr-client-config-reload-{}-{}.toml",
@@ -2446,22 +2496,29 @@ mod tests {
.unwrap()
.as_nanos()
));
- std::fs::write(&path, "[ui]\nredraw_on_focus_gained = false\n").unwrap();
+ std::fs::write(
+ &path,
+ "[ui]\nredraw_on_focus_gained = false\nhost_cursor = \"drawn\"\n",
+ )
+ .unwrap();
let path_string = path.to_string_lossy().to_string();
let _env = EnvVarGuard::set(crate::config::CONFIG_PATH_ENV_VAR, &path_string);
let mut sound_config = crate::config::SoundConfig::default();
let mut redraw_on_focus_gained = true;
+ let mut draw_host_cursor = false;
#[cfg(unix)]
let mut remote_image_paste_key = None;
reload_local_client_config(
&mut sound_config,
&mut redraw_on_focus_gained,
+ &mut draw_host_cursor,
#[cfg(unix)]
&mut remote_image_paste_key,
);
assert!(!redraw_on_focus_gained);
+ assert!(draw_host_cursor);
let _ = std::fs::remove_file(path);
}