aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-18 02:52:35 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-18 02:53:31 +0300
commit36b4001f4526b60f1c9b67a4a34596e5ad2bcca1 (patch)
tree520ab28a0a3f3f798e6e655fc83529e8617720e9 /src/client
parentaca35ea76f3df15ec68fd07959d939f5e7a78a6c (diff)
feat: sync theme with host appearance
refs #675
Diffstat (limited to 'src/client')
-rw-r--r--src/client/input.rs17
-rw-r--r--src/client/mod.rs112
2 files changed, 119 insertions, 10 deletions
diff --git a/src/client/input.rs b/src/client/input.rs
index add4990c..ba1e353f 100644
--- a/src/client/input.rs
+++ b/src/client/input.rs
@@ -59,6 +59,7 @@ fn unix_stdin_reader_loop(
let mut framer = crate::raw_input::RawInputByteFramer::default();
if host_color_query_sent {
framer.host_color_query_sent();
+ framer.enable_host_color_scheme_change_tracking();
}
while !should_quit.load(Ordering::Acquire) {
@@ -75,7 +76,10 @@ fn unix_stdin_reader_loop(
}
if stdin_read_ready(&reader, 10) == Some(false) {
- for data in framer.flush_timeout() {
+ let had_pending = framer.has_pending_input();
+ let chunks = framer.flush_timeout();
+ let held_escape = had_pending && chunks.is_empty();
+ for data in chunks {
if event_tx
.blocking_send(ClientLoopEvent::StdinInput(data))
.is_err()
@@ -83,6 +87,16 @@ fn unix_stdin_reader_loop(
return;
}
}
+ if held_escape && stdin_read_ready(&reader, 10) == Some(false) {
+ for data in framer.flush_timeout() {
+ if event_tx
+ .blocking_send(ClientLoopEvent::StdinInput(data))
+ .is_err()
+ {
+ return;
+ }
+ }
+ }
}
}
Err(err) => {
@@ -268,6 +282,7 @@ fn windows_client_input_event_from_raw(
Some(crate::protocol::ClientInputEvent::FocusLost)
}
crate::raw_input::RawInputEvent::HostDefaultColor { .. }
+ | crate::raw_input::RawInputEvent::HostColorSchemeChanged(_)
| crate::raw_input::RawInputEvent::Unsupported => None,
}
}
diff --git a/src/client/mod.rs b/src/client/mod.rs
index 70d39559..834edd2b 100644
--- a/src/client/mod.rs
+++ b/src/client/mod.rs
@@ -329,6 +329,8 @@ fn setup_terminal_with_capabilities(
mouse_capture: bool,
) -> io::Result<TerminalGuard> {
ratatui::init();
+ let host_color_scheme_reports =
+ should_enable_host_color_scheme_reports(enable_client_protocols);
if enable_client_protocols {
if mouse_capture {
@@ -337,11 +339,19 @@ fn setup_terminal_with_capabilities(
set_mouse_capture(false)?;
}
execute!(io::stdout(), EnableBracketedPaste, EnableFocusChange)?;
+ if host_color_scheme_reports {
+ write_host_color_scheme_report_mode(&mut io::stdout(), true)?;
+ }
push_keyboard_enhancement_flags()?;
- } else if mouse_capture {
- set_mouse_capture(true)?;
} else {
- set_mouse_capture(false)?;
+ if should_query_host_terminal_theme() {
+ write_host_color_scheme_report_mode(&mut io::stdout(), false)?;
+ }
+ if mouse_capture {
+ set_mouse_capture(true)?;
+ } else {
+ set_mouse_capture(false)?;
+ }
}
let modify_other_keys_mode = enable_client_protocols
@@ -360,15 +370,42 @@ fn setup_terminal_with_capabilities(
Ok(TerminalGuard {
reset_modify_other_keys: modify_other_keys_mode.is_some(),
+ reset_host_color_scheme_reports: host_color_scheme_reports,
})
}
+fn should_enable_host_color_scheme_reports(enable_client_protocols: bool) -> bool {
+ enable_client_protocols && should_query_host_terminal_theme()
+}
+
/// Guard that restores the terminal when dropped.
struct TerminalGuard {
reset_modify_other_keys: bool,
+ reset_host_color_scheme_reports: bool,
+}
+
+fn write_host_color_scheme_report_mode(
+ writer: &mut impl io::Write,
+ enabled: bool,
+) -> io::Result<()> {
+ let sequence = if enabled {
+ crate::terminal_theme::HOST_COLOR_SCHEME_REPORT_ENABLE_SEQUENCE
+ } else {
+ crate::terminal_theme::HOST_COLOR_SCHEME_REPORT_DISABLE_SEQUENCE
+ };
+ writer.write_all(sequence.as_bytes())?;
+ writer.flush()
}
-fn write_terminal_restore_postlude(writer: &mut impl io::Write) -> io::Result<()> {
+fn write_terminal_restore_postlude(
+ writer: &mut impl io::Write,
+ reset_host_color_scheme_reports: bool,
+) -> io::Result<()> {
+ if reset_host_color_scheme_reports {
+ writer.write_all(
+ crate::terminal_theme::HOST_COLOR_SCHEME_REPORT_DISABLE_SEQUENCE.as_bytes(),
+ )?;
+ }
// Restore a visible cursor and reset DECSCUSR back to the terminal default.
writer.write_all(b"\x1b[?25h\x1b[0 q")?;
writer.flush()
@@ -387,7 +424,7 @@ fn set_mouse_capture(enabled: bool) -> io::Result<()> {
}
}
-fn restore_terminal_state(reset_modify_other_keys: bool) {
+fn restore_terminal_state(reset_modify_other_keys: bool, reset_host_color_scheme_reports: bool) {
let _ = clear_received_kitty_graphics(&mut io::stdout());
// Reset modifyOtherKeys if we enabled it.
@@ -404,7 +441,7 @@ fn restore_terminal_state(reset_modify_other_keys: bool) {
DisableMouseCapture
);
ratatui::restore();
- let _ = write_terminal_restore_postlude(&mut io::stdout());
+ let _ = write_terminal_restore_postlude(&mut io::stdout(), reset_host_color_scheme_reports);
}
#[cfg(not(windows))]
@@ -432,7 +469,10 @@ fn pop_keyboard_enhancement_flags() -> io::Result<()> {
impl Drop for TerminalGuard {
fn drop(&mut self) {
- restore_terminal_state(self.reset_modify_other_keys);
+ restore_terminal_state(
+ self.reset_modify_other_keys,
+ self.reset_host_color_scheme_reports,
+ );
}
}
@@ -704,9 +744,13 @@ fn run_client_with_mode(
// Install a panic hook to restore the terminal on panic (same as monolithic).
let panic_resets_modify_other_keys = terminal_guard.reset_modify_other_keys;
+ let panic_resets_host_color_scheme_reports = terminal_guard.reset_host_color_scheme_reports;
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
- restore_terminal_state(panic_resets_modify_other_keys);
+ restore_terminal_state(
+ panic_resets_modify_other_keys,
+ panic_resets_host_color_scheme_reports,
+ );
original_hook(info);
}));
@@ -898,6 +942,9 @@ async fn run_client_loop(
) {
state.request_full_redraw();
}
+ if crate::raw_input::events_require_host_terminal_theme_query(&events) {
+ query_host_terminal_theme();
+ }
data
};
if should_bridge_clipboard_image_paste(&data, state.remote_image_paste_key) {
@@ -1601,17 +1648,64 @@ mod tests {
}
#[test]
+ fn write_host_color_scheme_report_mode_emits_mode_sequences() {
+ let mut output = Vec::new();
+ write_host_color_scheme_report_mode(&mut output, true).unwrap();
+ write_host_color_scheme_report_mode(&mut output, false).unwrap();
+
+ let mut expected = Vec::new();
+ expected.extend_from_slice(
+ crate::terminal_theme::HOST_COLOR_SCHEME_REPORT_ENABLE_SEQUENCE.as_bytes(),
+ );
+ expected.extend_from_slice(
+ crate::terminal_theme::HOST_COLOR_SCHEME_REPORT_DISABLE_SEQUENCE.as_bytes(),
+ );
+ assert_eq!(output, expected);
+ }
+
+ #[test]
+ fn color_scheme_change_event_requests_host_theme_query() {
+ let events = crate::raw_input::parse_raw_input_bytes_sync(b"\x1b[?997;1n");
+
+ assert!(crate::raw_input::events_require_host_terminal_theme_query(
+ &events
+ ));
+ }
+
+ #[test]
fn host_terminal_theme_query_is_disabled_on_windows() {
assert_eq!(should_query_host_terminal_theme(), !cfg!(windows));
}
#[test]
+ fn color_scheme_reports_are_enabled_only_for_full_clients() {
+ assert_eq!(
+ should_enable_host_color_scheme_reports(true),
+ !cfg!(windows)
+ );
+ assert!(!should_enable_host_color_scheme_reports(false));
+ }
+
+ #[test]
fn terminal_restore_postlude_restores_visible_default_cursor() {
let mut output = Vec::new();
- write_terminal_restore_postlude(&mut output).unwrap();
+ write_terminal_restore_postlude(&mut output, false).unwrap();
assert_eq!(output, b"\x1b[?25h\x1b[0 q");
}
+ #[test]
+ fn terminal_restore_postlude_disables_color_scheme_reports_when_enabled() {
+ let mut output = Vec::new();
+ write_terminal_restore_postlude(&mut output, true).unwrap();
+
+ let mut expected = Vec::new();
+ expected.extend_from_slice(
+ crate::terminal_theme::HOST_COLOR_SCHEME_REPORT_DISABLE_SEQUENCE.as_bytes(),
+ );
+ expected.extend_from_slice(b"\x1b[?25h\x1b[0 q");
+ assert_eq!(output, expected);
+ }
+
#[cfg(unix)]
#[test]
fn attach_escape_detaches_on_prefix_q() {