Restore reconnect status from snapshots
ci / test (push) Canceled after 0s
ci / fuzz-smoke (push) Canceled after 0s
ci / macos-client (macos-aarch64, macos-14) (push) Canceled after 0s
ci / macos-client (macos-x86_64, macos-13) (push) Canceled after 0s
ci / windows-client (push) Canceled after 0s
ci / package-release (linux-x86_64, ubuntu-latest, , , ) (push) Canceled after 0s
ci / package-release (macos-aarch64, macos-14, , , ) (push) Canceled after 0s
ci / package-release (macos-x86_64, macos-13, , , ) (push) Canceled after 0s
ci / package-release (windows-aarch64, windows-latest, aarch64, windows, aarch64-pc-windows-msvc) (push) Canceled after 0s
ci / package-release (windows-x86_64, windows-latest, , , ) (push) Canceled after 0s
ci / remote-bench (push) Canceled after 0s
ci / publish-gitea-release (push) Canceled after 0s

This commit is contained in:
DuProcess
2026-07-17 18:42:17 -04:00
parent 25c3358842
commit 89b81d73b1
+156 -66
View File
@@ -6560,6 +6560,7 @@ async fn run_terminal(
// Non-destructive disconnect status line. Off in forward-only mode (no TTY to // Non-destructive disconnect status line. Off in forward-only mode (no TTY to
// draw on) and respecting the client config (default on, env override). // draw on) and respecting the client config (default on, env override).
let mut disconnect_status = DisconnectStatus::new(resolve_disconnect_status() && !forward_only); let mut disconnect_status = DisconnectStatus::new(resolve_disconnect_status() && !forward_only);
let mut status_restore_pending = false;
let (forward_tx, mut forward_rx) = mpsc::channel::<ForwardEvent>(1024); let (forward_tx, mut forward_rx) = mpsc::channel::<ForwardEvent>(1024);
let _forward_keepalive = if local_forwards.is_empty() { let _forward_keepalive = if local_forwards.is_empty() {
Some(forward_tx.clone()) Some(forward_tx.clone())
@@ -6606,6 +6607,7 @@ async fn run_terminal(
if let Some(frame) = first_frame { if let Some(frame) = first_frame {
if !forward_only { if !forward_only {
render_frame(&frame)?; render_frame(&frame)?;
note_snapshot_rendered(&frame, &mut disconnect_status, &mut status_restore_pending);
predictor.observe_output(&frame.bytes); predictor.observe_output(&frame.bytes);
last_terminal_frame_at = Instant::now(); last_terminal_frame_at = Instant::now();
} }
@@ -6714,6 +6716,11 @@ async fn run_terminal(
); );
refresh_live_addr(&mut addr, &cred)?; refresh_live_addr(&mut addr, &cred)?;
render_frame(&frame)?; render_frame(&frame)?;
note_snapshot_rendered(
&frame,
&mut disconnect_status,
&mut status_restore_pending,
);
predictor.observe_output(&frame.bytes); predictor.observe_output(&frame.bytes);
last_terminal_frame_at = Instant::now(); last_terminal_frame_at = Instant::now();
last_packet_at = Instant::now(); last_packet_at = Instant::now();
@@ -6768,6 +6775,11 @@ async fn run_terminal(
&mut stale_terminal_input_suppress_until, &mut stale_terminal_input_suppress_until,
); );
render_frame(&frame)?; render_frame(&frame)?;
note_snapshot_rendered(
&frame,
&mut disconnect_status,
&mut status_restore_pending,
);
predictor.observe_output(&frame.bytes); predictor.observe_output(&frame.bytes);
last_terminal_frame_at = Instant::now(); last_terminal_frame_at = Instant::now();
last_packet_at = Instant::now(); last_packet_at = Instant::now();
@@ -6986,6 +6998,11 @@ async fn run_terminal(
); );
if !forward_only { if !forward_only {
render_frame(&frame)?; render_frame(&frame)?;
note_snapshot_rendered(
&frame,
&mut disconnect_status,
&mut status_restore_pending,
);
predictor.observe_output(&frame.bytes); predictor.observe_output(&frame.bytes);
last_terminal_frame_at = Instant::now(); last_terminal_frame_at = Instant::now();
} }
@@ -7053,6 +7070,11 @@ async fn run_terminal(
); );
if !forward_only { if !forward_only {
render_frame(&frame)?; render_frame(&frame)?;
note_snapshot_rendered(
&frame,
&mut disconnect_status,
&mut status_restore_pending,
);
predictor.observe_output(&frame.bytes); predictor.observe_output(&frame.bytes);
last_terminal_frame_at = Instant::now(); last_terminal_frame_at = Instant::now();
wake_repaint_retry_until = None; wake_repaint_retry_until = None;
@@ -7082,8 +7104,6 @@ async fn run_terminal(
if packet.header.conn_id != [0u8; 16] && packet.header.conn_id != cred.client_id { if packet.header.conn_id != [0u8; 16] && packet.header.conn_id != cred.client_id {
continue; continue;
} }
// Contact resumed: clear any disconnect status line immediately.
disconnect_status.apply(disconnect_status.on_contact())?;
match packet.header.kind { match packet.header.kind {
PacketKind::Frame | PacketKind::ResumeOk => { PacketKind::Frame | PacketKind::ResumeOk => {
let decrypted = protocol::decrypt_body(&packet, &cred.session_key, SERVER_TO_CLIENT) let decrypted = protocol::decrypt_body(&packet, &cred.session_key, SERVER_TO_CLIENT)
@@ -7112,6 +7132,11 @@ async fn run_terminal(
); );
if !forward_only { if !forward_only {
render_frame(&frame)?; render_frame(&frame)?;
note_snapshot_rendered(
&frame,
&mut disconnect_status,
&mut status_restore_pending,
);
predictor.observe_output(&frame.bytes); predictor.observe_output(&frame.bytes);
last_terminal_frame_at = Instant::now(); last_terminal_frame_at = Instant::now();
wake_repaint_retry_until = None; wake_repaint_retry_until = None;
@@ -7130,6 +7155,10 @@ async fn run_terminal(
} }
continue; continue;
}; };
note_authenticated_contact(
&mut disconnect_status,
&mut status_restore_pending,
)?;
let Ok(frame) = protocol::from_body::<Frame>(&plain) else { let Ok(frame) = protocol::from_body::<Frame>(&plain) else {
continue; continue;
}; };
@@ -7139,6 +7168,11 @@ async fn run_terminal(
predictor.clear_pending()?; predictor.clear_pending()?;
if !forward_only { if !forward_only {
render_frame(&frame)?; render_frame(&frame)?;
note_snapshot_rendered(
&frame,
&mut disconnect_status,
&mut status_restore_pending,
);
predictor.observe_output(&frame.bytes); predictor.observe_output(&frame.bytes);
last_terminal_frame_at = Instant::now(); last_terminal_frame_at = Instant::now();
wake_repaint_retry_until = None; wake_repaint_retry_until = None;
@@ -7161,6 +7195,25 @@ async fn run_terminal(
send_ack(&socket, addr, &cred, &mut send_seq).await?; send_ack(&socket, addr, &cred, &mut send_seq).await?;
} }
PacketKind::Pong => { PacketKind::Pong => {
if protocol::decrypt_body(
&packet,
&cred.session_key,
SERVER_TO_CLIENT,
)
.or_else(|err| match previous_session_key {
Some(prev) => {
protocol::decrypt_body(&packet, &prev, SERVER_TO_CLIENT)
}
None => Err(err),
})
.is_err()
{
continue;
}
note_authenticated_contact(
&mut disconnect_status,
&mut status_restore_pending,
)?;
last_packet_at = Instant::now(); last_packet_at = Instant::now();
if should_flush_terminal_input_after_contact( if should_flush_terminal_input_after_contact(
forward_only, forward_only,
@@ -7188,6 +7241,10 @@ async fn run_terminal(
let Ok(plain) = protocol::decrypt_body(&packet, &cred.session_key, SERVER_TO_CLIENT) else { let Ok(plain) = protocol::decrypt_body(&packet, &cred.session_key, SERVER_TO_CLIENT) else {
continue; continue;
}; };
note_authenticated_contact(
&mut disconnect_status,
&mut status_restore_pending,
)?;
let Ok(rekey) = protocol::from_body::<Rekey>(&plain) else { let Ok(rekey) = protocol::from_body::<Rekey>(&plain) else {
continue; continue;
}; };
@@ -7240,6 +7297,11 @@ async fn run_terminal(
); );
if !forward_only { if !forward_only {
render_frame(&frame)?; render_frame(&frame)?;
note_snapshot_rendered(
&frame,
&mut disconnect_status,
&mut status_restore_pending,
);
predictor.observe_output(&frame.bytes); predictor.observe_output(&frame.bytes);
last_terminal_frame_at = Instant::now(); last_terminal_frame_at = Instant::now();
wake_repaint_retry_until = None; wake_repaint_retry_until = None;
@@ -7800,7 +7862,9 @@ async fn run_terminal(
} }
let mut repainted_this_tick = false; let mut repainted_this_tick = false;
let stale = last_packet_at.elapsed(); let stale = last_packet_at.elapsed();
if stale >= Duration::from_secs(reconnect_timeout_secs.max(1)) { if status_restore_pending
|| stale >= Duration::from_secs(reconnect_timeout_secs.max(1))
{
if let Some(frame) = reconnect( if let Some(frame) = reconnect(
&socket, &socket,
&mut cred, &mut cred,
@@ -7817,6 +7881,11 @@ async fn run_terminal(
); );
if !forward_only { if !forward_only {
render_frame(&frame)?; render_frame(&frame)?;
note_snapshot_rendered(
&frame,
&mut disconnect_status,
&mut status_restore_pending,
);
predictor.observe_output(&frame.bytes); predictor.observe_output(&frame.bytes);
last_terminal_frame_at = Instant::now(); last_terminal_frame_at = Instant::now();
last_idle_repaint_attempt_at = Instant::now(); last_idle_repaint_attempt_at = Instant::now();
@@ -7896,6 +7965,11 @@ async fn run_terminal(
&mut stale_terminal_input_suppress_until, &mut stale_terminal_input_suppress_until,
); );
render_frame(&frame)?; render_frame(&frame)?;
note_snapshot_rendered(
&frame,
&mut disconnect_status,
&mut status_restore_pending,
);
predictor.observe_output(&frame.bytes); predictor.observe_output(&frame.bytes);
last_terminal_frame_at = Instant::now(); last_terminal_frame_at = Instant::now();
last_packet_at = Instant::now(); last_packet_at = Instant::now();
@@ -7913,7 +7987,7 @@ async fn run_terminal(
} }
} }
} }
// Refresh / clear the non-destructive disconnect status line based // Refresh or retire the reconnect status overlay based
// on how long the link has been silent (recomputed after any // on how long the link has been silent (recomputed after any
// reconnect attempt above may have reset `last_packet_at`). // reconnect attempt above may have reset `last_packet_at`).
if !forward_only { if !forward_only {
@@ -7922,6 +7996,9 @@ async fn run_terminal(
} else { } else {
disconnect_status.on_tick(last_packet_at.elapsed()) disconnect_status.on_tick(last_packet_at.elapsed())
}; };
if action == StatusAction::Forget {
status_restore_pending = true;
}
disconnect_status.apply(action)?; disconnect_status.apply(action)?;
} }
} }
@@ -7972,7 +8049,7 @@ async fn run_terminal(
} }
} }
// Leave the terminal clean on exit: erase any lingering status line. // Leave the terminal clean on exit: erase any lingering status line.
let _ = disconnect_status.apply(StatusAction::Clear); let _ = disconnect_status.apply(StatusAction::Forget);
let _ = detach_once(&socket, &cred, send_seq).await; let _ = detach_once(&socket, &cred, send_seq).await;
Ok(()) Ok(())
} }
@@ -10175,7 +10252,7 @@ fn input_matches_escape(bytes: &[u8], escape_key: Option<&[u8]>) -> bool {
!escape_key.is_empty() && bytes.windows(escape_key.len()).any(|w| w == escape_key) !escape_key.is_empty() && bytes.windows(escape_key.len()).any(|w| w == escape_key)
} }
/// Resolve whether the non-destructive disconnect status line is enabled. /// Resolve whether the snapshot-restored disconnect status line is enabled.
/// `DOSH_DISCONNECT_STATUS` (0/false/off to disable) overrides for quick /// `DOSH_DISCONNECT_STATUS` (0/false/off to disable) overrides for quick
/// experiments; otherwise the client config's `disconnect_status` applies, /// experiments; otherwise the client config's `disconnect_status` applies,
/// defaulting to on. /// defaulting to on.
@@ -10400,18 +10477,18 @@ const TERMINAL_SNAPSHOT_RESET: &[u8] = concat!(
/// idle period (the run loop only pings after 2s of quiet) never flashes it. /// idle period (the run loop only pings after 2s of quiet) never flashes it.
const DISCONNECT_STATUS_THRESHOLD_SECS: u64 = 2; const DISCONNECT_STATUS_THRESHOLD_SECS: u64 = 2;
/// Non-destructive, mosh-style disconnect status bar. /// Mosh-style disconnect status bar with snapshot-backed restoration.
/// ///
/// When server packets stop arriving we paint one blue line on the terminal's /// When server packets stop arriving we paint one blue line on the terminal's
/// top row — e.g. `[dosh] reconnecting — last contact 3s ago` — using /// top row — e.g. `[dosh] reconnecting — last contact 3s ago` — using
/// save/restore cursor (ESC 7 / ESC 8) so the real application cursor never /// save/restore cursor (ESC 7 / ESC 8) so the real application cursor never
/// moves. The run loop suppresses the bar while the server is in the alternate /// moves. Painting row 1 necessarily replaces its cells, so authenticated
/// screen because a saved cursor does not make writing row 1 non-destructive for /// recovery retires the overlay without clearing the row and requests an
/// full-screen TUIs. The bar is refreshed on the status timer tick and cleared /// authoritative server snapshot to restore it. The run loop suppresses the bar
/// the instant a packet resumes. /// while the server is in the alternate screen.
/// ///
/// The decision logic and the rendered text are pure and unit-tested; only /// The decision logic and the rendered text are pure and unit-tested; only
/// [`DisconnectStatus::emit`] / [`DisconnectStatus::emit_clear`] touch stdout. /// [`DisconnectStatus::emit`] touches stdout.
struct DisconnectStatus { struct DisconnectStatus {
enabled: bool, enabled: bool,
/// Whether a status line is currently painted on screen (so we know to clear /// Whether a status line is currently painted on screen (so we know to clear
@@ -10429,12 +10506,32 @@ enum StatusAction {
None, None,
/// Paint/repaint the status bar with this exact text on the top row. /// Paint/repaint the status bar with this exact text on the top row.
Show(String), Show(String),
/// Erase the status line (link recovered or feature disabled).
Clear,
/// Drop tracked status state without touching the terminal. /// Drop tracked status state without touching the terminal.
Forget, Forget,
} }
fn note_authenticated_contact(
status: &mut DisconnectStatus,
restore_pending: &mut bool,
) -> Result<()> {
let action = status.on_contact();
if action == StatusAction::Forget {
*restore_pending = true;
}
status.apply(action)
}
fn note_snapshot_rendered(
frame: &Frame,
status: &mut DisconnectStatus,
restore_pending: &mut bool,
) {
if frame.snapshot {
status.applied(&StatusAction::Forget);
*restore_pending = false;
}
}
impl DisconnectStatus { impl DisconnectStatus {
fn new(enabled: bool) -> Self { fn new(enabled: bool) -> Self {
Self { Self {
@@ -10462,7 +10559,7 @@ impl DisconnectStatus {
fn on_tick(&self, stale: Duration) -> StatusAction { fn on_tick(&self, stale: Duration) -> StatusAction {
if !self.enabled { if !self.enabled {
return if self.shown { return if self.shown {
StatusAction::Clear StatusAction::Forget
} else { } else {
StatusAction::None StatusAction::None
}; };
@@ -10475,17 +10572,17 @@ impl DisconnectStatus {
StatusAction::Show(text) StatusAction::Show(text)
} }
} else if self.shown { } else if self.shown {
StatusAction::Clear StatusAction::Forget
} else { } else {
StatusAction::None StatusAction::None
} }
} }
/// Pure decision for "a packet just arrived": clear any visible status line /// Pure decision for authenticated contact. The caller schedules a snapshot
/// the instant contact resumes, otherwise do nothing. /// repaint before forgetting a visible overlay; it never clears row 1.
fn on_contact(&self) -> StatusAction { fn on_contact(&self) -> StatusAction {
if self.shown { if self.shown {
StatusAction::Clear StatusAction::Forget
} else { } else {
StatusAction::None StatusAction::None
} }
@@ -10510,10 +10607,6 @@ impl DisconnectStatus {
self.shown = true; self.shown = true;
self.drawn_text = text.clone(); self.drawn_text = text.clone();
} }
StatusAction::Clear => {
self.shown = false;
self.drawn_text.clear();
}
StatusAction::Forget => { StatusAction::Forget => {
self.shown = false; self.shown = false;
self.drawn_text.clear(); self.drawn_text.clear();
@@ -10526,7 +10619,6 @@ impl DisconnectStatus {
fn apply(&mut self, action: StatusAction) -> Result<()> { fn apply(&mut self, action: StatusAction) -> Result<()> {
match &action { match &action {
StatusAction::Show(text) => self.emit(text)?, StatusAction::Show(text) => self.emit(text)?,
StatusAction::Clear => self.emit_clear()?,
StatusAction::Forget => {} StatusAction::Forget => {}
StatusAction::None => {} StatusAction::None => {}
} }
@@ -10541,12 +10633,6 @@ impl DisconnectStatus {
let bytes = render_status_overlay(text, rows); let bytes = render_status_overlay(text, rows);
emit_status(&bytes) emit_status(&bytes)
} }
/// Erase the top-row status bar, again with save/restore cursor.
fn emit_clear(&self) -> Result<()> {
let (_, rows) = terminal_size();
emit_status(&render_status_clear(rows))
}
} }
/// Pure: build the escape sequence that paints `text` on a top blue status bar, /// Pure: build the escape sequence that paints `text` on a top blue status bar,
@@ -10563,16 +10649,6 @@ fn render_status_overlay(text: &str, _rows: u16) -> Vec<u8> {
out out
} }
/// Pure: build the escape sequence that clears the top status bar.
fn render_status_clear(_rows: u16) -> Vec<u8> {
let mut out = Vec::with_capacity(12);
out.extend_from_slice(b"\x1b7");
out.extend_from_slice(b"\x1b[1;1H");
out.extend_from_slice(b"\x1b[2K");
out.extend_from_slice(b"\x1b8");
out
}
#[cfg(not(test))] #[cfg(not(test))]
fn emit_status(bytes: &[u8]) -> Result<()> { fn emit_status(bytes: &[u8]) -> Result<()> {
let mut stdout = std::io::stdout(); let mut stdout = std::io::stdout();
@@ -10879,14 +10955,14 @@ mod tests {
is_local_status_target, is_resume_response_for_client, latest_release_download_url, is_local_status_target, is_resume_response_for_client, latest_release_download_url,
load_first_native_identity_with_prompt, local_symlink_target_is_dir, load_first_native_identity_with_prompt, local_symlink_target_is_dir,
local_username_from_env, native_proxy_udp_warning, newest_client_trace_path_from, local_username_from_env, native_proxy_udp_warning, newest_client_trace_path_from,
parse_dynamic_forward, parse_escape_key, parse_local_forward, parse_remote_forward, note_authenticated_contact, note_snapshot_rendered, parse_dynamic_forward,
parse_single_remote_path, parse_ssh_config, parse_trace_line, parse_trace_options, parse_escape_key, parse_local_forward, parse_remote_forward, parse_single_remote_path,
parse_trace_report_options, parse_trace_summary, parse_update_options, parse_ssh_config, parse_trace_line, parse_trace_options, parse_trace_report_options,
post_submit_hold_duration, queue_or_send_stream_data, queue_pending_user_input, parse_trace_summary, parse_update_options, post_submit_hold_duration,
queue_stale_pending_user_input, raw_contains_host_table, recv_response_until, queue_or_send_stream_data, queue_pending_user_input, queue_stale_pending_user_input,
refresh_live_addr, release_artifact_name_for, release_tag_download_url, raw_contains_host_table, recv_response_until, refresh_live_addr, release_artifact_name_for,
release_tag_from_effective_url, release_version_from_tag, remote_unix_server_update_script, release_tag_download_url, release_tag_from_effective_url, release_version_from_tag,
render_frame_bytes, render_status_clear, render_status_overlay, requested_env, remote_unix_server_update_script, render_frame_bytes, render_status_overlay, requested_env,
resolve_forward_agent_endpoint, resolved_startup_command, retire_stream_state, resolve_forward_agent_endpoint, resolved_startup_command, retire_stream_state,
retransmit_stream_closes, retransmit_stream_eofs, retransmit_stream_opens, retransmit_stream_closes, retransmit_stream_eofs, retransmit_stream_opens,
retransmit_stream_window_adjusts, rewrite_forward_command, sanitize_trace_name, retransmit_stream_window_adjusts, rewrite_forward_command, sanitize_trace_name,
@@ -14374,10 +14450,10 @@ mod tests {
); );
} }
/// Contact resuming clears a shown line immediately, and a sub-threshold tick /// Contact retires a shown overlay without erasing row 1; the run loop then
/// also clears it; clearing when nothing is shown is a no-op. /// restores the covered cells from an authenticated snapshot.
#[test] #[test]
fn disconnect_status_clears_on_contact_and_recovery() { fn disconnect_status_forgets_on_contact_and_recovery() {
let mut status = DisconnectStatus::new(true); let mut status = DisconnectStatus::new(true);
// Nothing shown yet: contact is a no-op. // Nothing shown yet: contact is a no-op.
@@ -14387,27 +14463,29 @@ mod tests {
status.applied(&show); status.applied(&show);
assert!(status.shown); assert!(status.shown);
// A packet arrives: clear immediately. let forget = status.on_contact();
let clear = status.on_contact(); assert_eq!(forget, StatusAction::Forget);
assert_eq!(clear, StatusAction::Clear); status.applied(&forget);
status.applied(&clear);
assert!(!status.shown); assert!(!status.shown);
// Show again, then a sub-threshold tick (link recovered) clears it too. // A sub-threshold authenticated recovery also requests restoration.
let show = status.on_tick(Duration::from_secs(5)); let show = status.on_tick(Duration::from_secs(5));
status.applied(&show); status.applied(&show);
assert_eq!(status.on_tick(Duration::from_secs(0)), StatusAction::Clear); assert_eq!(status.on_tick(Duration::from_secs(0)), StatusAction::Forget);
} }
/// When the feature is disabled the machine never shows, and clears anything /// When disabled the machine never shows, and retires stale state without
/// already on screen (e.g. config toggled off at runtime). /// writing destructive row-clearing sequences.
#[test] #[test]
fn disconnect_status_disabled_never_shows() { fn disconnect_status_disabled_never_shows() {
let mut status = DisconnectStatus::new(false); let mut status = DisconnectStatus::new(false);
assert_eq!(status.on_tick(Duration::from_secs(10)), StatusAction::None); assert_eq!(status.on_tick(Duration::from_secs(10)), StatusAction::None);
// Simulate a stale "shown" flag and confirm it gets cleared. // Simulate a stale "shown" flag and confirm it requests restoration.
status.shown = true; status.shown = true;
assert_eq!(status.on_tick(Duration::from_secs(10)), StatusAction::Clear); assert_eq!(
status.on_tick(Duration::from_secs(10)),
StatusAction::Forget
);
} }
#[test] #[test]
@@ -14424,6 +14502,23 @@ mod tests {
assert_eq!(status.drawn_text, ""); assert_eq!(status.drawn_text, "");
} }
#[test]
fn authenticated_contact_requires_snapshot_to_restore_covered_row() {
let mut status = DisconnectStatus::new(true);
let show = status.on_tick(Duration::from_secs(3));
status.applied(&show);
let mut restore_pending = false;
note_authenticated_contact(&mut status, &mut restore_pending).unwrap();
assert!(!status.shown);
assert!(restore_pending);
note_snapshot_rendered(&test_frame(1, false), &mut status, &mut restore_pending);
assert!(restore_pending);
note_snapshot_rendered(&test_frame(2, true), &mut status, &mut restore_pending);
assert!(!restore_pending);
}
/// The rendered overlay must preserve cursor position: save cursor (ESC 7), /// The rendered overlay must preserve cursor position: save cursor (ESC 7),
/// park on the top row, paint the blue reconnect bar, then restore cursor /// park on the top row, paint the blue reconnect bar, then restore cursor
/// (ESC 8). The run loop suppresses this renderer entirely in full-screen /// (ESC 8). The run loop suppresses this renderer entirely in full-screen
@@ -14441,11 +14536,6 @@ mod tests {
assert!(bytes.windows(8).any(|w| w == b"\x1b[44;37m")); assert!(bytes.windows(8).any(|w| w == b"\x1b[44;37m"));
// Resets attributes before restoring the cursor. // Resets attributes before restoring the cursor.
assert!(bytes.windows(4).any(|w| w == b"\x1b[0m")); assert!(bytes.windows(4).any(|w| w == b"\x1b[0m"));
let clear = render_status_clear(24);
assert!(clear.starts_with(b"\x1b7"));
assert!(clear.ends_with(b"\x1b8"));
assert!(clear.windows(4).any(|w| w == b"\x1b[2K"));
} }
#[test] #[test]