diff --git a/src/bin/dosh-client.rs b/src/bin/dosh-client.rs index e2c66ac..c38b150 100644 --- a/src/bin/dosh-client.rs +++ b/src/bin/dosh-client.rs @@ -9418,11 +9418,39 @@ async fn detach_once(socket: &UdpSocket, cred: &CachedCredential, seq: u64) -> R fn render_frame(frame: &Frame) -> Result<()> { let mut stdout = std::io::stdout(); - stdout.write_all(&frame.bytes)?; + stdout.write_all(&render_frame_bytes(frame))?; stdout.flush()?; Ok(()) } +fn render_frame_bytes(frame: &Frame) -> Vec { + if !frame.snapshot { + return frame.bytes.clone(); + } + let mut bytes = Vec::with_capacity(TERMINAL_SNAPSHOT_RESET.len() + frame.bytes.len()); + bytes.extend_from_slice(TERMINAL_SNAPSHOT_RESET); + bytes.extend_from_slice(&frame.bytes); + bytes +} + +const TERMINAL_SNAPSHOT_RESET: &[u8] = concat!( + "\x1b[0m", + "\x1b[?25h", + "\x1b[?1003l", + "\x1b[?1002l", + "\x1b[?1001l", + "\x1b[?1000l", + "\x1b[?1004l", + "\x1b[?1015l", + "\x1b[?1006l", + "\x1b[?1005l", + "\x1b[?2004l", + "\x1b[?47l", + "\x1b[?1047l", + "\x1b[?1049l" +) +.as_bytes(); + /// Seconds of silence from the server before the disconnect status line appears. /// Short enough to give quick feedback on a lost link, long enough that a normal /// idle period (the run loop only pings after 2s of quiet) never flashes it. @@ -9754,11 +9782,11 @@ mod tests { NativeIdentityContext, POST_RECONNECT_STALE_INPUT_GRACE, POST_SUBMIT_ALL_INPUT_HOLD, PendingStreamOpen, PredictMode, Predictor, RESTART_STATUS_SCRIPT, RemoteForward, STALE_TERMINAL_INPUT_AFTER, STARTUP_INPUT_HOLD, STREAM_INITIAL_WINDOW, SshConfig, - SshPathTokenContext, StartupGateMode, StatusAction, UpdateOptions, UpdateRole, auth_allows, - cache_key, cache_server_prefix, cleanup_stream_state, clear_cached_credentials, - effective_update_artifact_tag, ensure_tui_safe_status_overlay, expand_ssh_path_tokens, - input_contains_focus_in, input_matches_escape, is_local_status_target, - is_resume_response_for_client, latest_release_download_url, + SshPathTokenContext, StartupGateMode, StatusAction, TERMINAL_SNAPSHOT_RESET, UpdateOptions, + UpdateRole, auth_allows, cache_key, cache_server_prefix, cleanup_stream_state, + clear_cached_credentials, effective_update_artifact_tag, ensure_tui_safe_status_overlay, + expand_ssh_path_tokens, input_contains_focus_in, input_matches_escape, + is_local_status_target, is_resume_response_for_client, latest_release_download_url, load_first_native_identity_with_prompt, native_proxy_udp_warning, newest_client_trace_path_from, parse_dynamic_forward, parse_escape_key, parse_local_forward, parse_remote_forward, parse_single_remote_path, parse_ssh_config, @@ -9766,9 +9794,9 @@ mod tests { parse_update_options, post_submit_hold_duration, queue_or_send_stream_data, queue_pending_user_input, queue_stale_pending_user_input, raw_contains_host_table, recv_response_until, refresh_live_addr, release_tag_download_url, - release_tag_from_effective_url, release_version_from_tag, render_status_clear, - render_status_overlay, requested_env, resolved_startup_command, retire_stream_state, - retransmit_stream_opens, rewrite_forward_command, sanitize_trace_name, + release_tag_from_effective_url, release_version_from_tag, render_frame_bytes, + render_status_clear, render_status_overlay, requested_env, resolved_startup_command, + retire_stream_state, retransmit_stream_opens, rewrite_forward_command, sanitize_trace_name, selected_predict_mode, selected_udp_host, send_stream_eof, server_version_mismatch, should_flush_terminal_input_after_contact, should_health_log_client_start, should_hold_during_startup_gate, should_hold_post_submit_input, @@ -10424,6 +10452,22 @@ mod tests { } } + #[test] + fn snapshot_render_resets_local_terminal_modes_before_authoritative_bytes() { + let frame = test_frame(7, true); + let bytes = render_frame_bytes(&frame); + + assert!(bytes.starts_with(TERMINAL_SNAPSHOT_RESET)); + assert!(bytes.ends_with(b"frame-7")); + } + + #[test] + fn live_frame_render_preserves_terminal_output_exactly() { + let frame = test_frame(7, false); + + assert_eq!(render_frame_bytes(&frame), b"frame-7"); + } + #[test] fn frame_buffer_renders_only_contiguous_frames() { let mut buffer = FrameBuffer::default();