Harden alternate screen tracking
ci / test (push) Has been cancelled
ci / fuzz-smoke (push) Has been cancelled
ci / windows-client (push) Has been cancelled
ci / package-release (linux-x86_64, ubuntu-latest) (push) Has been cancelled
ci / package-release (macos-aarch64, macos-14) (push) Has been cancelled
ci / package-release (macos-x86_64, macos-13) (push) Has been cancelled
ci / package-release (windows-x86_64, windows-latest) (push) Has been cancelled
ci / remote-bench (push) Has been cancelled

This commit is contained in:
DuProcess
2026-07-07 20:37:16 -04:00
parent c1c672b188
commit 4c9e31fd16
3 changed files with 134 additions and 16 deletions
Generated
+1 -1
View File
@@ -436,7 +436,7 @@ dependencies = [
[[package]] [[package]]
name = "dosh" name = "dosh"
version = "1.0.0-rc8" version = "1.0.0-rc9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "dosh" name = "dosh"
version = "1.0.0-rc8" version = "1.0.0-rc9"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
+132 -14
View File
@@ -6296,6 +6296,60 @@ const FLAG_TRIGGER_LOW_MS: f64 = 50.0;
/// (covers a sudden latency spike on the very first slow keystroke). /// (covers a sudden latency spike on the very first slow keystroke).
const GLITCH_FORCE_MS: u128 = 250; const GLITCH_FORCE_MS: u128 = 250;
/// Enough recent bytes to join a CSI private-mode sequence split across frames
/// without retaining arbitrary terminal output.
const TERMINAL_OUTPUT_PARSE_TAIL: usize = 64;
fn alternate_screen_after_output(mut active: bool, bytes: &[u8]) -> bool {
let mut offset = 0usize;
while offset < bytes.len() {
let Some((next, transition)) = terminal_private_mode_transition(bytes, offset) else {
offset += 1;
continue;
};
if let Some(enable) = transition {
active = enable;
}
offset = next.max(offset + 1);
}
active
}
fn terminal_private_mode_transition(bytes: &[u8], offset: usize) -> Option<(usize, Option<bool>)> {
let mut cursor = offset;
match bytes.get(cursor).copied()? {
0x1b if bytes.get(cursor + 1) == Some(&b'[') => cursor += 2,
0x9b => cursor += 1,
_ => return None,
}
if bytes.get(cursor) != Some(&b'?') {
return None;
}
cursor += 1;
let params_start = cursor;
while let Some(byte) = bytes.get(cursor).copied() {
match byte {
b'0'..=b'9' | b';' | b':' => cursor += 1,
b'h' | b'l' => {
let params = &bytes[params_start..cursor];
let touches_alt = terminal_private_params_include_alt_screen(params);
return Some((cursor + 1, touches_alt.then_some(byte == b'h')));
}
0x40..=0x7e => return Some((cursor + 1, None)),
_ => return None,
}
}
None
}
fn terminal_private_params_include_alt_screen(params: &[u8]) -> bool {
params
.split(|byte| matches!(byte, b';' | b':'))
.any(|param| matches!(param, b"47" | b"1047" | b"1049"))
}
/// One speculatively-echoed character on the current line. /// One speculatively-echoed character on the current line.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct PredictedCell { struct PredictedCell {
@@ -6313,6 +6367,9 @@ struct Predictor {
/// as vim/htop); we never speculate there because we cannot model arbitrary /// as vim/htop); we never speculate there because we cannot model arbitrary
/// cursor addressing safely. /// cursor addressing safely.
alternate_screen: bool, alternate_screen: bool,
/// Tail of recent terminal output, retained so alternate-screen transitions
/// split across UDP frames are still detected.
output_parse_tail: Vec<u8>,
/// Predicted cells for the current line, left-to-right starting at the /// Predicted cells for the current line, left-to-right starting at the
/// column where the cursor sat when this run of predictions began. /// column where the cursor sat when this run of predictions began.
@@ -6359,6 +6416,7 @@ impl Predictor {
mode, mode,
enabled: enabled && mode != PredictMode::Off, enabled: enabled && mode != PredictMode::Off,
alternate_screen: false, alternate_screen: false,
output_parse_tail: Vec::new(),
cells: Vec::new(), cells: Vec::new(),
cursor: 0, cursor: 0,
epoch: 1, epoch: 1,
@@ -6381,6 +6439,7 @@ impl Predictor {
self.epoch += 1; self.epoch += 1;
self.confirmed_epoch = self.epoch - 1; self.confirmed_epoch = self.epoch - 1;
self.alternate_screen = false; self.alternate_screen = false;
self.output_parse_tail.clear();
self.oldest_pending_at = None; self.oldest_pending_at = None;
} }
@@ -6537,20 +6596,20 @@ impl Predictor {
/// and confirms outstanding predictions: a fresh frame means the server has /// and confirms outstanding predictions: a fresh frame means the server has
/// re-rendered the line, so our speculative glyphs are superseded and erased. /// re-rendered the line, so our speculative glyphs are superseded and erased.
fn observe_output(&mut self, bytes: &[u8]) { fn observe_output(&mut self, bytes: &[u8]) {
if contains_bytes(bytes, b"\x1b[?1049h") let mut parse = Vec::with_capacity(self.output_parse_tail.len() + bytes.len());
|| contains_bytes(bytes, b"\x1b[?1047h") parse.extend_from_slice(&self.output_parse_tail);
|| contains_bytes(bytes, b"\x1b[?47h") parse.extend_from_slice(bytes);
{
self.alternate_screen = true; let before = self.alternate_screen;
let _ = self.discard_all(); self.alternate_screen = alternate_screen_after_output(self.alternate_screen, &parse);
} if self.alternate_screen != before {
if contains_bytes(bytes, b"\x1b[?1049l")
|| contains_bytes(bytes, b"\x1b[?1047l")
|| contains_bytes(bytes, b"\x1b[?47l")
{
self.alternate_screen = false;
let _ = self.discard_all(); let _ = self.discard_all();
} }
self.output_parse_tail.clear();
let keep = parse.len().min(TERMINAL_OUTPUT_PARSE_TAIL);
self.output_parse_tail
.extend_from_slice(&parse[parse.len().saturating_sub(keep)..]);
} }
/// Sample SRTT from a newly arrived frame and confirm/clear outstanding /// Sample SRTT from a newly arrived frame and confirm/clear outstanding
@@ -7344,10 +7403,13 @@ const TERMINAL_CLEANUP: &[u8] = concat!(
"\x1b[?1002l", "\x1b[?1002l",
"\x1b[?1001l", "\x1b[?1001l",
"\x1b[?1000l", "\x1b[?1000l",
"\x1b[?1004l",
"\x1b[?1015l", "\x1b[?1015l",
"\x1b[?1006l", "\x1b[?1006l",
"\x1b[?1005l", "\x1b[?1005l",
"\x1b[?2004l", "\x1b[?2004l",
"\x1b[?47l",
"\x1b[?1047l",
"\x1b[?1049l" "\x1b[?1049l"
) )
.as_bytes(); .as_bytes();
@@ -7372,8 +7434,9 @@ mod tests {
should_hold_during_startup_gate, should_hold_post_submit_input, should_hold_during_startup_gate, should_hold_post_submit_input,
should_repaint_idle_alternate_screen, split_after_command_submit, ssh_destination_host, should_repaint_idle_alternate_screen, split_after_command_submit, ssh_destination_host,
ssh_username, ssh_with_user, startup_command, status_ssh_target, strip_stale_mouse_reports, ssh_username, ssh_with_user, startup_command, status_ssh_target, strip_stale_mouse_reports,
strip_terminal_focus_reports, toml_bare_key_or_quoted, update_check_requested, strip_terminal_focus_reports, terminal_private_mode_transition, toml_bare_key_or_quoted,
update_version_status, upsert_managed_block, valid_forward_host, vscode_safe_alias, update_check_requested, update_version_status, upsert_managed_block, valid_forward_host,
vscode_safe_alias,
}; };
use dosh::config::{ClientConfig, CommandExtension, HostConfig}; use dosh::config::{ClientConfig, CommandExtension, HostConfig};
use dosh::native::EnvVar; use dosh::native::EnvVar;
@@ -7892,6 +7955,61 @@ mod tests {
assert!(!predictor.alternate_screen); assert!(!predictor.alternate_screen);
} }
#[test]
fn predictor_tracks_legacy_and_combined_alternate_screen_modes() {
let mut predictor = Predictor::new(true);
predictor.observe_output(b"\x1b[?1000;1006;1049h");
assert!(predictor.alternate_screen);
predictor.observe_output(b"\x1b[?1006;1000l");
assert!(
predictor.alternate_screen,
"leaving mouse modes must not imply leaving alternate screen"
);
predictor.observe_output(b"\x1b[?1047l");
assert!(!predictor.alternate_screen);
predictor.observe_output(b"\x1b[?47h");
assert!(predictor.alternate_screen);
predictor.observe_output(b"\x1b[?47l");
assert!(!predictor.alternate_screen);
}
#[test]
fn predictor_tracks_alternate_screen_split_across_frames() {
let mut predictor = Predictor::new(true);
predictor.observe_output(b"\x1b");
assert!(!predictor.alternate_screen);
predictor.observe_output(b"[?");
assert!(!predictor.alternate_screen);
predictor.observe_output(b"10");
assert!(!predictor.alternate_screen);
predictor.observe_output(b"49h");
assert!(predictor.alternate_screen);
predictor.observe_output(b"\x1b");
assert!(predictor.alternate_screen);
predictor.observe_output(b"[?104");
assert!(predictor.alternate_screen);
predictor.observe_output(b"9l");
assert!(!predictor.alternate_screen);
}
#[test]
fn private_mode_parser_ignores_non_alt_modes() {
assert_eq!(
terminal_private_mode_transition(b"\x1b[?1000;1006h", 0),
Some((13, None))
);
assert_eq!(
terminal_private_mode_transition(b"\x1b[?1000;1049h", 0),
Some((13, Some(true)))
);
}
/// Typing printable characters builds an in-order predicted line and the /// Typing printable characters builds an in-order predicted line and the
/// local cursor tracks the end of it. /// local cursor tracks the end of it.
#[test] #[test]