Skip to content

Commit 28ec9d8

Browse files
ttak0422claude
andauthored
fix: improve session snapshot and terminal lifecycle management (#17)
* fix: improve session snapshot and lifecycle management - (H) Send terminal cleanup sequences on detach to reset mouse modes, bracketed paste, alternate screen, and cursor visibility in the client terminal - (B) Increase scrollback capacity from 0 to 10,000 lines so history is available after reattach - (G) Respond to DA1/DA2 queries with canned responses when no clients are connected, preventing apps from hanging on capability detection - (A) Prepend ESC[?1049h in snapshots when alternate screen is active so reattaching clients properly enter alternate screen mode - (C) Track window title via vt100 Callbacks and restore it in snapshots via OSC 2 sequence on reattach Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: minor formatting and comment cleanup in server.rs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f433990 commit 28ec9d8

File tree

3 files changed

+98
-7
lines changed

3 files changed

+98
-7
lines changed

src/bridge.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ use std::sync::atomic::{AtomicBool, Ordering};
1818
const TOKEN_STDIN: Token = Token(0);
1919
const TOKEN_SOCKET: Token = Token(1);
2020
const TOKEN_WAKE: Token = Token(2);
21+
const DETACH_CLEANUP_SEQUENCES: &[u8] =
22+
b"\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?2004l\x1b[?1049l\x1b[?25h";
2123

2224
static SIGWINCH_RECEIVED: AtomicBool = AtomicBool::new(false);
2325

@@ -311,6 +313,7 @@ pub fn run(
311313
// Send DETACH before exiting
312314
let msg = proto::encode(proto::client::DETACH, &[]);
313315
let _ = socket.write_all(&msg);
316+
let _ = write_all_raw(stdout_fd, DETACH_CLEANUP_SEQUENCES);
314317

315318
Ok(exit_code)
316319
}

src/server.rs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ use std::time::Duration;
1111
const LISTENER: Token = Token(0);
1212
const PTY_BASE: Token = Token(0x1000_0000);
1313
const CLIENT_BASE: Token = Token(0x2000_0000);
14+
const DA1_RESPONSE: &[u8] = b"\x1b[?62;22c"; // Primary Device Attributes (DA1)
15+
const DA2_RESPONSE: &[u8] = b"\x1b[>1;10;0c"; // Secondary Device Attributes (DA2)
1416

1517
struct Client {
1618
stream: UnixStream,
@@ -236,6 +238,22 @@ impl Server {
236238
}
237239
}
238240

241+
let (pending_da1, pending_da2) = self.session.take_pending_da_queries();
242+
if self.clients.is_empty() {
243+
for _ in 0..pending_da1 {
244+
if let Err(e) = self.session.write_pty(DA1_RESPONSE) {
245+
log::warn!("Failed to write DA1 response to PTY: {}", e);
246+
break;
247+
}
248+
}
249+
for _ in 0..pending_da2 {
250+
if let Err(e) = self.session.write_pty(DA2_RESPONSE) {
251+
log::warn!("Failed to write DA2 response to PTY: {}", e);
252+
break;
253+
}
254+
}
255+
}
256+
239257
if !self.pending_pty_output.is_empty() {
240258
self.flush_pty_output();
241259
}
@@ -264,10 +282,7 @@ impl Server {
264282
.filter_map(|(&id, c)| if c.pending_snapshot { Some(id) } else { None })
265283
.collect();
266284
for id in &snapshot_ids {
267-
log::info!(
268-
"Client {} snapshot triggered by PTY output arrival",
269-
*id
270-
);
285+
log::info!("Client {} snapshot triggered by PTY output arrival", *id);
271286
self.send_snapshot_to_client(*id);
272287
}
273288

src/session.rs

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,57 @@ use crate::pty::Pty;
22
use std::io;
33
use std::os::fd::AsRawFd;
44

5+
#[derive(Default)]
6+
struct SessionCallbacks {
7+
window_title: Option<String>,
8+
pending_da1_queries: usize,
9+
pending_da2_queries: usize,
10+
}
11+
12+
impl SessionCallbacks {
13+
fn default_da_params(params: &[&[u16]]) -> bool {
14+
params.is_empty()
15+
|| (params.len() == 1
16+
&& (params[0].is_empty() || (params[0].len() == 1 && params[0][0] == 0)))
17+
}
18+
19+
fn take_pending_da_queries(&mut self) -> (usize, usize) {
20+
let counts = (self.pending_da1_queries, self.pending_da2_queries);
21+
self.pending_da1_queries = 0;
22+
self.pending_da2_queries = 0;
23+
counts
24+
}
25+
}
26+
27+
impl vt100::Callbacks for SessionCallbacks {
28+
fn set_window_title(&mut self, _: &mut vt100::Screen, title: &[u8]) {
29+
self.window_title = Some(String::from_utf8_lossy(title).into_owned());
30+
}
31+
32+
fn unhandled_csi(
33+
&mut self,
34+
_: &mut vt100::Screen,
35+
i1: Option<u8>,
36+
i2: Option<u8>,
37+
params: &[&[u16]],
38+
c: char,
39+
) {
40+
if c != 'c' || i2.is_some() || !Self::default_da_params(params) {
41+
return;
42+
}
43+
44+
match i1 {
45+
None => self.pending_da1_queries += 1,
46+
Some(b'>') => self.pending_da2_queries += 1,
47+
_ => {}
48+
}
49+
}
50+
}
51+
552
pub struct Session {
653
pub name: String,
754
pub pty: Pty,
8-
parser: vt100::Parser,
55+
parser: vt100::Parser<SessionCallbacks>,
956
pub exited: Option<i32>,
1057
}
1158

@@ -16,7 +63,12 @@ impl Session {
1663
Ok(Self {
1764
name,
1865
pty,
19-
parser: vt100::Parser::new(rows, cols, 0),
66+
parser: vt100::Parser::new_with_callbacks(
67+
rows,
68+
cols,
69+
10_000,
70+
SessionCallbacks::default(),
71+
),
2072
exited: None,
2173
})
2274
}
@@ -70,7 +122,28 @@ impl Session {
70122

71123
/// Generate escape sequences that reproduce the current terminal state.
72124
pub fn snapshot(&self) -> Vec<u8> {
73-
self.parser.screen().state_formatted()
125+
let mut snapshot = self.parser.screen().state_formatted();
126+
let mut prefix = Vec::new();
127+
128+
if let Some(title) = self.parser.callbacks().window_title.as_ref() {
129+
prefix.extend_from_slice(b"\x1b]2;");
130+
prefix.extend_from_slice(title.as_bytes());
131+
prefix.extend_from_slice(b"\x1b\\");
132+
}
133+
if self.parser.screen().alternate_screen() {
134+
prefix.extend_from_slice(b"\x1b[?1049h");
135+
}
136+
137+
if prefix.is_empty() {
138+
snapshot
139+
} else {
140+
prefix.append(&mut snapshot);
141+
prefix
142+
}
143+
}
144+
145+
pub fn take_pending_da_queries(&mut self) -> (usize, usize) {
146+
self.parser.callbacks_mut().take_pending_da_queries()
74147
}
75148

76149
/// Get the master fd for polling.

0 commit comments

Comments
 (0)