Compare commits
2
Commits
v1.0.0-rc5
...
v1.0.0-rc6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37ec9fc520 | ||
|
|
f3c7ec225d |
Generated
+1
-1
@@ -436,7 +436,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dosh"
|
||||
version = "1.0.0-rc5"
|
||||
version = "1.0.0-rc6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dosh"
|
||||
version = "1.0.0-rc5"
|
||||
version = "1.0.0-rc6"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
|
||||
|
||||
+139
-10
@@ -66,6 +66,8 @@ const STARTUP_INPUT_HOLD: Duration = Duration::from_millis(750);
|
||||
const POST_SUBMIT_ALL_INPUT_HOLD: Duration = Duration::from_millis(120);
|
||||
const STALE_TERMINAL_INPUT_AFTER: Duration = Duration::from_secs(2);
|
||||
const POST_RECONNECT_STALE_INPUT_GRACE: Duration = Duration::from_secs(2);
|
||||
const FOCUS_REPAINT_COOLDOWN: Duration = Duration::from_secs(1);
|
||||
const ALT_SCREEN_IDLE_REPAINT_AFTER: Duration = Duration::from_secs(15);
|
||||
|
||||
/// Sentinel `target_host` the server uses on a server-initiated `StreamOpen` that
|
||||
/// represents an SSH-agent connection (rather than a TCP target). The client
|
||||
@@ -4334,10 +4336,14 @@ async fn run_terminal(
|
||||
let mut startup_input_hold_until: Option<Instant> = None;
|
||||
let mut startup_gate_mode = StartupGateMode::HoldControl;
|
||||
let mut stale_terminal_input_suppress_until: Option<Instant> = None;
|
||||
let mut last_terminal_frame_at = Instant::now();
|
||||
let mut last_focus_repaint_at = Instant::now() - FOCUS_REPAINT_COOLDOWN;
|
||||
let mut last_idle_repaint_attempt_at = Instant::now() - ALT_SCREEN_IDLE_REPAINT_AFTER;
|
||||
if let Some(frame) = first_frame {
|
||||
if !forward_only {
|
||||
render_frame(&frame)?;
|
||||
predictor.observe_output(&frame.bytes);
|
||||
last_terminal_frame_at = Instant::now();
|
||||
}
|
||||
if frame.closed {
|
||||
return Ok(());
|
||||
@@ -4395,6 +4401,41 @@ async fn run_terminal(
|
||||
if input_matches_escape(&bytes, escape_key.as_deref()) {
|
||||
break;
|
||||
}
|
||||
if !forward_only
|
||||
&& input_contains_focus_in(&bytes)
|
||||
&& last_focus_repaint_at.elapsed() >= FOCUS_REPAINT_COOLDOWN
|
||||
{
|
||||
last_focus_repaint_at = Instant::now();
|
||||
if let Some(frame) = reconnect(
|
||||
&socket,
|
||||
&mut cred,
|
||||
&mut send_seq,
|
||||
last_size,
|
||||
&mut frame_buffer,
|
||||
&mut predictor,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
refresh_live_addr(&mut addr, &cred)?;
|
||||
arm_stale_terminal_input_suppression(
|
||||
&mut stale_terminal_input_suppress_until,
|
||||
);
|
||||
render_frame(&frame)?;
|
||||
predictor.observe_output(&frame.bytes);
|
||||
last_terminal_frame_at = Instant::now();
|
||||
last_packet_at = Instant::now();
|
||||
flush_pending_user_input(
|
||||
&socket,
|
||||
addr,
|
||||
&cred,
|
||||
&mut send_seq,
|
||||
&mut predictor,
|
||||
&mut pending_user_input,
|
||||
&mut pending_user_input_bytes,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
if should_strip_stale_terminal_reports(
|
||||
last_packet_at.elapsed(),
|
||||
stale_terminal_input_suppress_until,
|
||||
@@ -4508,6 +4549,7 @@ async fn run_terminal(
|
||||
if !forward_only {
|
||||
render_frame(&frame)?;
|
||||
predictor.observe_output(&frame.bytes);
|
||||
last_terminal_frame_at = Instant::now();
|
||||
}
|
||||
last_packet_at = Instant::now();
|
||||
flush_pending_user_input(
|
||||
@@ -4570,6 +4612,7 @@ async fn run_terminal(
|
||||
if !forward_only {
|
||||
render_frame(&frame)?;
|
||||
predictor.observe_output(&frame.bytes);
|
||||
last_terminal_frame_at = Instant::now();
|
||||
}
|
||||
last_packet_at = Instant::now();
|
||||
flush_pending_user_input(
|
||||
@@ -4623,6 +4666,7 @@ async fn run_terminal(
|
||||
if !forward_only {
|
||||
render_frame(&frame)?;
|
||||
predictor.observe_output(&frame.bytes);
|
||||
last_terminal_frame_at = Instant::now();
|
||||
}
|
||||
last_packet_at = Instant::now();
|
||||
flush_pending_user_input(
|
||||
@@ -4648,6 +4692,7 @@ async fn run_terminal(
|
||||
if !forward_only {
|
||||
render_frame(&frame)?;
|
||||
predictor.observe_output(&frame.bytes);
|
||||
last_terminal_frame_at = Instant::now();
|
||||
flush_startup_input_if_ready(
|
||||
&socket,
|
||||
addr,
|
||||
@@ -4747,6 +4792,7 @@ async fn run_terminal(
|
||||
if !forward_only {
|
||||
render_frame(&frame)?;
|
||||
predictor.observe_output(&frame.bytes);
|
||||
last_terminal_frame_at = Instant::now();
|
||||
}
|
||||
last_packet_at = Instant::now();
|
||||
flush_pending_user_input(
|
||||
@@ -5142,6 +5188,7 @@ async fn run_terminal(
|
||||
if !forward_only {
|
||||
render_frame(&frame)?;
|
||||
predictor.observe_output(&frame.bytes);
|
||||
last_terminal_frame_at = Instant::now();
|
||||
}
|
||||
last_packet_at = Instant::now();
|
||||
flush_pending_user_input(
|
||||
@@ -5183,6 +5230,44 @@ async fn run_terminal(
|
||||
(&mut startup_input_hold_until, &mut startup_gate_mode),
|
||||
)
|
||||
.await?;
|
||||
let now = Instant::now();
|
||||
if should_repaint_idle_alternate_screen(
|
||||
predictor.alternate_screen,
|
||||
last_terminal_frame_at,
|
||||
last_idle_repaint_attempt_at,
|
||||
now,
|
||||
) {
|
||||
last_idle_repaint_attempt_at = now;
|
||||
if let Some(frame) = reconnect(
|
||||
&socket,
|
||||
&mut cred,
|
||||
&mut send_seq,
|
||||
last_size,
|
||||
&mut frame_buffer,
|
||||
&mut predictor,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
refresh_live_addr(&mut addr, &cred)?;
|
||||
arm_stale_terminal_input_suppression(
|
||||
&mut stale_terminal_input_suppress_until,
|
||||
);
|
||||
render_frame(&frame)?;
|
||||
predictor.observe_output(&frame.bytes);
|
||||
last_terminal_frame_at = Instant::now();
|
||||
last_packet_at = Instant::now();
|
||||
flush_pending_user_input(
|
||||
&socket,
|
||||
addr,
|
||||
&cred,
|
||||
&mut send_seq,
|
||||
&mut predictor,
|
||||
&mut pending_user_input,
|
||||
&mut pending_user_input_bytes,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Refresh / clear the non-destructive disconnect status line based
|
||||
// on how long the link has been silent (recomputed after any
|
||||
@@ -5552,6 +5637,21 @@ fn should_strip_stale_terminal_reports(
|
||||
|| suppress_until.is_some_and(|deadline| Instant::now() < deadline)
|
||||
}
|
||||
|
||||
fn input_contains_focus_in(bytes: &[u8]) -> bool {
|
||||
contains_bytes(bytes, b"\x1b[I") || contains_bytes(bytes, b"\x9bI")
|
||||
}
|
||||
|
||||
fn should_repaint_idle_alternate_screen(
|
||||
alternate_screen: bool,
|
||||
last_terminal_frame_at: Instant,
|
||||
last_attempt_at: Instant,
|
||||
now: Instant,
|
||||
) -> bool {
|
||||
alternate_screen
|
||||
&& now.duration_since(last_terminal_frame_at) >= ALT_SCREEN_IDLE_REPAINT_AFTER
|
||||
&& now.duration_since(last_attempt_at) >= ALT_SCREEN_IDLE_REPAINT_AFTER
|
||||
}
|
||||
|
||||
fn arm_stale_terminal_input_suppression(suppress_until: &mut Option<Instant>) {
|
||||
*suppress_until = Some(Instant::now() + POST_RECONNECT_STALE_INPUT_GRACE);
|
||||
}
|
||||
@@ -7230,11 +7330,12 @@ const TERMINAL_CLEANUP: &[u8] = concat!(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
CachedCredential, DisconnectStatus, DynamicForward, FRAME_GAP_RESYNC_AFTER_MS, FrameBuffer,
|
||||
LocalForward, MAX_PENDING_USER_INPUT_BYTES, POST_SUBMIT_ALL_INPUT_HOLD, PendingStreamOpen,
|
||||
PredictMode, Predictor, RESTART_STATUS_SCRIPT, RemoteForward, STARTUP_INPUT_HOLD,
|
||||
SshConfig, StartupGateMode, StatusAction, auth_allows, cache_key, cache_server_prefix,
|
||||
clear_cached_credentials, ensure_tui_safe_status_overlay, input_matches_escape,
|
||||
ALT_SCREEN_IDLE_REPAINT_AFTER, CachedCredential, DisconnectStatus, DynamicForward,
|
||||
FRAME_GAP_RESYNC_AFTER_MS, FrameBuffer, LocalForward, MAX_PENDING_USER_INPUT_BYTES,
|
||||
POST_SUBMIT_ALL_INPUT_HOLD, PendingStreamOpen, PredictMode, Predictor,
|
||||
RESTART_STATUS_SCRIPT, RemoteForward, STARTUP_INPUT_HOLD, SshConfig, StartupGateMode,
|
||||
StatusAction, auth_allows, cache_key, cache_server_prefix, clear_cached_credentials,
|
||||
ensure_tui_safe_status_overlay, 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, parse_dynamic_forward, parse_escape_key,
|
||||
parse_local_forward, parse_remote_forward, parse_ssh_config, post_submit_hold_duration,
|
||||
@@ -7243,10 +7344,11 @@ mod tests {
|
||||
render_status_clear, render_status_overlay, requested_env, resolved_startup_command,
|
||||
retransmit_stream_opens, rewrite_forward_command, selected_predict_mode, selected_udp_host,
|
||||
server_version_mismatch, should_flush_terminal_input_after_contact,
|
||||
should_hold_during_startup_gate, should_hold_post_submit_input, split_after_command_submit,
|
||||
ssh_destination_host, ssh_username, ssh_with_user, startup_command, status_ssh_target,
|
||||
strip_stale_mouse_reports, toml_bare_key_or_quoted, update_check_requested,
|
||||
update_version_status, upsert_managed_block, valid_forward_host, vscode_safe_alias,
|
||||
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,
|
||||
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;
|
||||
@@ -7255,7 +7357,7 @@ mod tests {
|
||||
use ed25519_dalek::{SigningKey, VerifyingKey};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::fs;
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
fn test_args(server: &str, command: &[&str]) -> super::Args {
|
||||
super::Args {
|
||||
@@ -8551,6 +8653,33 @@ mod tests {
|
||||
assert_eq!(strip_stale_mouse_reports(b"\x9bIhello\x9bO"), b"hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_in_reports_trigger_local_repaint_detection() {
|
||||
assert!(input_contains_focus_in(b"\x1b[I"));
|
||||
assert!(input_contains_focus_in(b"before\x9bIafter"));
|
||||
assert!(!input_contains_focus_in(b"\x1b[O"));
|
||||
assert!(!input_contains_focus_in(b"\x1b[A"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idle_repaint_only_runs_for_stale_alternate_screen() {
|
||||
let now = Instant::now();
|
||||
let stale = now - ALT_SCREEN_IDLE_REPAINT_AFTER - Duration::from_secs(1);
|
||||
let recent = now - Duration::from_secs(1);
|
||||
assert!(should_repaint_idle_alternate_screen(
|
||||
true, stale, stale, now
|
||||
));
|
||||
assert!(!should_repaint_idle_alternate_screen(
|
||||
false, stale, stale, now
|
||||
));
|
||||
assert!(!should_repaint_idle_alternate_screen(
|
||||
true, recent, stale, now
|
||||
));
|
||||
assert!(!should_repaint_idle_alternate_screen(
|
||||
true, stale, recent, now
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_mouse_escape_input_survives_stale_filter() {
|
||||
assert_eq!(strip_stale_mouse_reports(b"\x1b[A"), b"\x1b[A");
|
||||
|
||||
Reference in New Issue
Block a user