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
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:
+132
-14
@@ -6296,6 +6296,60 @@ const FLAG_TRIGGER_LOW_MS: f64 = 50.0;
|
||||
/// (covers a sudden latency spike on the very first slow keystroke).
|
||||
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.
|
||||
#[derive(Clone, Debug)]
|
||||
struct PredictedCell {
|
||||
@@ -6313,6 +6367,9 @@ struct Predictor {
|
||||
/// as vim/htop); we never speculate there because we cannot model arbitrary
|
||||
/// cursor addressing safely.
|
||||
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
|
||||
/// column where the cursor sat when this run of predictions began.
|
||||
@@ -6359,6 +6416,7 @@ impl Predictor {
|
||||
mode,
|
||||
enabled: enabled && mode != PredictMode::Off,
|
||||
alternate_screen: false,
|
||||
output_parse_tail: Vec::new(),
|
||||
cells: Vec::new(),
|
||||
cursor: 0,
|
||||
epoch: 1,
|
||||
@@ -6381,6 +6439,7 @@ impl Predictor {
|
||||
self.epoch += 1;
|
||||
self.confirmed_epoch = self.epoch - 1;
|
||||
self.alternate_screen = false;
|
||||
self.output_parse_tail.clear();
|
||||
self.oldest_pending_at = None;
|
||||
}
|
||||
|
||||
@@ -6537,20 +6596,20 @@ impl Predictor {
|
||||
/// and confirms outstanding predictions: a fresh frame means the server has
|
||||
/// re-rendered the line, so our speculative glyphs are superseded and erased.
|
||||
fn observe_output(&mut self, bytes: &[u8]) {
|
||||
if contains_bytes(bytes, b"\x1b[?1049h")
|
||||
|| contains_bytes(bytes, b"\x1b[?1047h")
|
||||
|| contains_bytes(bytes, b"\x1b[?47h")
|
||||
{
|
||||
self.alternate_screen = true;
|
||||
let _ = self.discard_all();
|
||||
}
|
||||
if contains_bytes(bytes, b"\x1b[?1049l")
|
||||
|| contains_bytes(bytes, b"\x1b[?1047l")
|
||||
|| contains_bytes(bytes, b"\x1b[?47l")
|
||||
{
|
||||
self.alternate_screen = false;
|
||||
let mut parse = Vec::with_capacity(self.output_parse_tail.len() + bytes.len());
|
||||
parse.extend_from_slice(&self.output_parse_tail);
|
||||
parse.extend_from_slice(bytes);
|
||||
|
||||
let before = self.alternate_screen;
|
||||
self.alternate_screen = alternate_screen_after_output(self.alternate_screen, &parse);
|
||||
if self.alternate_screen != before {
|
||||
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
|
||||
@@ -7344,10 +7403,13 @@ const TERMINAL_CLEANUP: &[u8] = concat!(
|
||||
"\x1b[?1002l",
|
||||
"\x1b[?1001l",
|
||||
"\x1b[?1000l",
|
||||
"\x1b[?1004l",
|
||||
"\x1b[?1015l",
|
||||
"\x1b[?1006l",
|
||||
"\x1b[?1005l",
|
||||
"\x1b[?2004l",
|
||||
"\x1b[?47l",
|
||||
"\x1b[?1047l",
|
||||
"\x1b[?1049l"
|
||||
)
|
||||
.as_bytes();
|
||||
@@ -7372,8 +7434,9 @@ mod tests {
|
||||
should_hold_during_startup_gate, should_hold_post_submit_input,
|
||||
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,
|
||||
strip_terminal_focus_reports, toml_bare_key_or_quoted, update_check_requested,
|
||||
update_version_status, upsert_managed_block, valid_forward_host, vscode_safe_alias,
|
||||
strip_terminal_focus_reports, terminal_private_mode_transition, toml_bare_key_or_quoted,
|
||||
update_check_requested, update_version_status, upsert_managed_block, valid_forward_host,
|
||||
vscode_safe_alias,
|
||||
};
|
||||
use dosh::config::{ClientConfig, CommandExtension, HostConfig};
|
||||
use dosh::native::EnvVar;
|
||||
@@ -7892,6 +7955,61 @@ mod tests {
|
||||
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
|
||||
/// local cursor tracks the end of it.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user