Make restart reconnect persistent
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:
+25
-29
@@ -691,7 +691,7 @@ async fn run_selftest_command(config: &dosh::config::ClientConfig, args: &Args)
|
||||
|
||||
let overlay = render_status_overlay("[dosh] reconnecting — last contact 3s ago", 24);
|
||||
ensure_tui_safe_status_overlay(&overlay)?;
|
||||
println!("[ok] disconnect UI: save/restore bottom-row overlay");
|
||||
println!("[ok] disconnect UI: save/restore top status bar");
|
||||
|
||||
parse_local_forward("18080:127.0.0.1:80")?;
|
||||
parse_remote_forward("19090:127.0.0.1:5432")?;
|
||||
@@ -1659,9 +1659,9 @@ fn select_session(requested: Option<&str>, _force_new: bool) -> String {
|
||||
if let Some(session) = requested {
|
||||
return session.to_string();
|
||||
}
|
||||
// Implicit, ephemeral session: the server recognizes this name shape
|
||||
// (`protocol::is_implicit_session_name`) and does NOT persist it across
|
||||
// restarts, since you can never reattach to a random name.
|
||||
// Implicit session: this gives plain `dosh host` a fresh terminal by
|
||||
// default. The server can still persist it across restarts because the
|
||||
// reconnect ticket cache keeps the generated name.
|
||||
protocol::generate_implicit_session_name()
|
||||
}
|
||||
|
||||
@@ -4735,16 +4735,13 @@ fn render_frame(frame: &Frame) -> Result<()> {
|
||||
/// idle period (the run loop only pings after 2s of quiet) never flashes it.
|
||||
const DISCONNECT_STATUS_THRESHOLD_SECS: u64 = 2;
|
||||
|
||||
/// Non-destructive, mosh-style disconnect status line.
|
||||
/// Non-destructive, mosh-style disconnect status bar.
|
||||
///
|
||||
/// When server packets stop arriving we paint a single, unobtrusive line on the
|
||||
/// terminal's *bottom row* — e.g. reverse-video cyan `[dosh] last contact 3s ago
|
||||
/// — reconnecting…` — using save/restore cursor (ESC 7 / ESC 8) so the real
|
||||
/// application cursor never moves and full-screen TUIs are not corrupted. (The
|
||||
/// old in-band overlay wrote over the active line and corrupted TUIs; this draws
|
||||
/// only the last row, parks the cursor there, then restores, touching nothing
|
||||
/// the app owns.) The line is refreshed on the status timer tick and cleared the
|
||||
/// instant a packet resumes.
|
||||
/// 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
|
||||
/// save/restore cursor (ESC 7 / ESC 8) so the real application cursor never
|
||||
/// moves and full-screen TUIs are not corrupted. The bar is refreshed on the
|
||||
/// status timer tick and cleared the instant a packet resumes.
|
||||
///
|
||||
/// The decision logic and the rendered text are pure and unit-tested; only
|
||||
/// [`DisconnectStatus::emit`] / [`DisconnectStatus::emit_clear`] touch stdout.
|
||||
@@ -4763,7 +4760,7 @@ struct DisconnectStatus {
|
||||
enum StatusAction {
|
||||
/// Leave the screen as-is (already correct).
|
||||
None,
|
||||
/// Paint/repaint the status line with this exact text on the bottom row.
|
||||
/// Paint/repaint the status bar with this exact text on the top row.
|
||||
Show(String),
|
||||
/// Erase the status line (link recovered or feature disabled).
|
||||
Clear,
|
||||
@@ -4868,26 +4865,25 @@ impl DisconnectStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pure: build the escape sequence that paints `text` on `rows`-tall terminal's
|
||||
/// bottom row with reverse-video cyan, leaving the cursor where it was.
|
||||
fn render_status_overlay(text: &str, rows: u16) -> Vec<u8> {
|
||||
/// Pure: build the escape sequence that paints `text` on a top blue status bar,
|
||||
/// leaving the cursor where it was.
|
||||
fn render_status_overlay(text: &str, _rows: u16) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(text.len() + 24);
|
||||
out.extend_from_slice(b"\x1b7"); // save cursor
|
||||
// Move to bottom row, column 1.
|
||||
out.extend_from_slice(format!("\x1b[{};1H", rows.max(1)).as_bytes());
|
||||
out.extend_from_slice(b"\x1b[1;1H"); // move to top row, column 1
|
||||
out.extend_from_slice(b"\x1b[2K"); // clear entire row
|
||||
out.extend_from_slice(b"\x1b[7;36m"); // reverse video + cyan
|
||||
out.extend_from_slice(b"\x1b[44;37m"); // blue background + white text
|
||||
out.extend_from_slice(text.as_bytes());
|
||||
out.extend_from_slice(b"\x1b[0m"); // reset attributes
|
||||
out.extend_from_slice(b"\x1b8"); // restore cursor
|
||||
out
|
||||
}
|
||||
|
||||
/// Pure: build the escape sequence that clears the bottom-row status line.
|
||||
fn render_status_clear(rows: u16) -> Vec<u8> {
|
||||
/// 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(format!("\x1b[{};1H", rows.max(1)).as_bytes());
|
||||
out.extend_from_slice(b"\x1b[1;1H");
|
||||
out.extend_from_slice(b"\x1b[2K");
|
||||
out.extend_from_slice(b"\x1b8");
|
||||
out
|
||||
@@ -5993,19 +5989,19 @@ mod tests {
|
||||
}
|
||||
|
||||
/// The rendered overlay must be non-destructive: save cursor (ESC 7), park on
|
||||
/// the bottom row, paint reverse-video cyan, then restore cursor (ESC 8) so
|
||||
/// the top row, paint the blue reconnect bar, then restore cursor (ESC 8) so
|
||||
/// the application's cursor and full-screen TUIs are never disturbed.
|
||||
#[test]
|
||||
fn disconnect_status_overlay_is_save_restore_bottom_row() {
|
||||
fn disconnect_status_overlay_is_save_restore_top_blue_bar() {
|
||||
let bytes = render_status_overlay("[dosh] reconnecting — last contact 3s ago", 24);
|
||||
ensure_tui_safe_status_overlay(&bytes).unwrap();
|
||||
assert!(bytes.starts_with(b"\x1b7"), "must save cursor first");
|
||||
assert!(bytes.ends_with(b"\x1b8"), "must restore cursor last");
|
||||
// Positions to the last row (row 24), column 1.
|
||||
assert!(bytes.windows(7).any(|w| w == b"\x1b[24;1H"));
|
||||
// Clears the row and uses reverse-video (7) + cyan (36).
|
||||
// Positions to the first row, column 1.
|
||||
assert!(bytes.windows(6).any(|w| w == b"\x1b[1;1H"));
|
||||
// Clears the row and uses a blue background with white text.
|
||||
assert!(bytes.windows(4).any(|w| w == b"\x1b[2K"));
|
||||
assert!(bytes.windows(7).any(|w| w == b"\x1b[7;36m"));
|
||||
assert!(bytes.windows(8).any(|w| w == b"\x1b[44;37m"));
|
||||
// Resets attributes before restoring the cursor.
|
||||
assert!(bytes.windows(4).any(|w| w == b"\x1b[0m"));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user