Skip to main content

blit_server/
lib.rs

1use blit_alacritty::{SearchResult as AlacrittySearchResult, TerminalDriver as AlacrittyDriver};
2use blit_remote::{
3    build_update_msg, msg_hello, FrameState, C2S_ACK, C2S_CLIENT_METRICS, C2S_CLOSE, C2S_CREATE,
4    C2S_CREATE2, C2S_CREATE_AT, C2S_CREATE_N, C2S_DISPLAY_RATE, C2S_FOCUS, C2S_INPUT, C2S_MOUSE,
5    C2S_KILL, C2S_READ, C2S_RESIZE, C2S_RESTART, C2S_SCROLL, C2S_SEARCH, C2S_SUBSCRIBE,
6    C2S_UNSUBSCRIBE,
7    CREATE2_HAS_COMMAND, CREATE2_HAS_SRC_PTY, FEATURE_CREATE_NONCE, FEATURE_RESIZE_BATCH,
8    FEATURE_RESTART, READ_ANSI, READ_TAIL, S2C_CLOSED, S2C_CREATED, S2C_CREATED_N, S2C_LIST,
9    S2C_READY, S2C_SEARCH_RESULTS, S2C_TEXT, S2C_TITLE,
10};
11use std::collections::{HashMap, HashSet, VecDeque};
12use std::ffi::CString;
13use std::os::unix::fs::PermissionsExt;
14use std::os::unix::io::RawFd;
15use std::sync::Arc;
16use std::time::{Duration, Instant};
17use tokio::io::unix::AsyncFd;
18use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
19use tokio::net::UnixListener;
20use tokio::sync::{mpsc, Mutex, Notify};
21
22type PtyFds = Arc<std::sync::RwLock<HashMap<u16, RawFd>>>;
23pub struct Config {
24    pub shell: String,
25    pub shell_flags: String,
26    pub scrollback: usize,
27    pub socket_path: String,
28    pub fd_channel: Option<RawFd>,
29}
30
31fn pty_write_all(fd: libc::c_int, mut data: &[u8]) {
32    while !data.is_empty() {
33        let ret = unsafe { libc::write(fd, data.as_ptr().cast(), data.len()) };
34        if ret > 0 {
35            data = &data[ret as usize..];
36        } else if ret < 0 {
37            let err = std::io::Error::last_os_error();
38            if err.kind() == std::io::ErrorKind::Interrupted {
39                continue;
40            }
41            break;
42        } else {
43            break;
44        }
45    }
46}
47
48trait PtyDriver: Send {
49    fn size(&self) -> (u16, u16);
50    fn resize(&mut self, rows: u16, cols: u16);
51    fn process(&mut self, data: &[u8]);
52    fn title(&self) -> &str;
53    fn search_result(&self, query: &str) -> Option<PtySearchResult>;
54    fn take_title_dirty(&mut self) -> bool;
55    fn cursor_position(&self) -> (u16, u16);
56    fn synced_output(&self) -> bool;
57    fn snapshot(&mut self, echo: bool, icanon: bool) -> FrameState;
58    fn scrollback_frame(&mut self, offset: usize) -> FrameState;
59    fn reset_modes(&mut self);
60    fn mouse_event(
61        &self,
62        type_: u8,
63        button: u8,
64        col: u16,
65        row: u16,
66        echo: bool,
67        icanon: bool,
68    ) -> Option<Vec<u8>>;
69}
70
71struct PtySearchResult {
72    score: u32,
73    primary_source: u8,
74    matched_sources: u8,
75    context: String,
76    scroll_offset: Option<usize>,
77}
78
79impl PtyDriver for AlacrittyDriver {
80    fn size(&self) -> (u16, u16) {
81        AlacrittyDriver::size(self)
82    }
83
84    fn resize(&mut self, rows: u16, cols: u16) {
85        AlacrittyDriver::resize(self, rows, cols);
86    }
87
88    fn process(&mut self, data: &[u8]) {
89        AlacrittyDriver::process(self, data);
90    }
91
92    fn title(&self) -> &str {
93        AlacrittyDriver::title(self)
94    }
95
96    fn search_result(&self, query: &str) -> Option<PtySearchResult> {
97        AlacrittyDriver::search_result(self, query).map(|result: AlacrittySearchResult| {
98            PtySearchResult {
99                score: result.score,
100                primary_source: result.primary_source as u8,
101                matched_sources: result.matched_sources,
102                context: result.context,
103                scroll_offset: result.scroll_offset,
104            }
105        })
106    }
107
108    fn take_title_dirty(&mut self) -> bool {
109        AlacrittyDriver::take_title_dirty(self)
110    }
111
112    fn cursor_position(&self) -> (u16, u16) {
113        AlacrittyDriver::cursor_position(self)
114    }
115
116    fn synced_output(&self) -> bool {
117        AlacrittyDriver::synced_output(self)
118    }
119
120    fn snapshot(&mut self, echo: bool, icanon: bool) -> FrameState {
121        AlacrittyDriver::snapshot(self, echo, icanon)
122    }
123
124    fn scrollback_frame(&mut self, offset: usize) -> FrameState {
125        AlacrittyDriver::scrollback_frame(self, offset)
126    }
127
128    fn reset_modes(&mut self) {
129        AlacrittyDriver::reset_modes(self);
130    }
131
132    fn mouse_event(
133        &self,
134        type_: u8,
135        button: u8,
136        col: u16,
137        row: u16,
138        echo: bool,
139        icanon: bool,
140    ) -> Option<Vec<u8>> {
141        AlacrittyDriver::mouse_event(self, type_, button, col, row, echo, icanon)
142    }
143}
144
145// Keep small to limit bufferbloat on slow connections.  The soft queue limit
146// (OUTBOX_SOFT_QUEUE_LIMIT_FRAMES) prevents the tick from queuing more than
147// ~2 frames, so this just needs to be bigger than that with some headroom.
148const OUTBOX_CAPACITY: usize = 8;
149const OUTBOX_SOFT_QUEUE_LIMIT_FRAMES: usize = 2;
150const PREVIEW_FRAME_RESERVE: usize = 1;
151const READY_FRAME_QUEUE_CAP: usize = 4;
152const PTY_CHANNEL_CAPACITY: usize = 64;
153const SYNC_OUTPUT_END: &[u8] = b"\x1b[?2026l";
154
155/// A chunk of data from the PTY reader, sent through a lock-free channel
156/// so the reader never contends with the delivery tick for the Session mutex.
157enum PtyInput {
158    /// Raw bytes from the PTY, with the reader's sync-scan tail for boundary
159    /// detection. The tick task calls `process()` + `respond_to_queries()`.
160    Data(Vec<u8>),
161    /// Data up to a sync-output-close boundary. `before` should be processed
162    /// and then a snapshot taken. `after` is remainder for the next chunk.
163    SyncBoundary { before: Vec<u8>, after: Vec<u8> },
164    /// The PTY fd hit EOF or an error — the child likely exited.
165    Eof,
166}
167
168const MAX_FRAME_SIZE: usize = 16 * 1024 * 1024;
169
170async fn read_frame(reader: &mut (impl AsyncRead + Unpin)) -> Option<Vec<u8>> {
171    let mut len_buf = [0u8; 4];
172    reader.read_exact(&mut len_buf).await.ok()?;
173    let len = u32::from_le_bytes(len_buf) as usize;
174    if len == 0 {
175        return Some(vec![]);
176    }
177    if len > MAX_FRAME_SIZE {
178        return None;
179    }
180    let mut buf = vec![0u8; len];
181    reader.read_exact(&mut buf).await.ok()?;
182    Some(buf)
183}
184
185async fn write_frame(writer: &mut (impl AsyncWrite + Unpin), payload: &[u8]) -> bool {
186    if payload.len() > u32::MAX as usize {
187        return false;
188    }
189    let len = payload.len() as u32;
190    let mut buf = Vec::with_capacity(4 + payload.len());
191    buf.extend_from_slice(&len.to_le_bytes());
192    buf.extend_from_slice(payload);
193    writer.write_all(&buf).await.is_ok()
194}
195
196struct Pty {
197    master_fd: libc::c_int,
198    child_pid: libc::pid_t,
199    driver: Box<dyn PtyDriver>,
200    /// Client-chosen tag set at creation time.
201    tag: String,
202    dirty: bool,
203    ready_frames: VecDeque<FrameState>,
204    /// Receives raw byte chunks from the PTY reader task without mutex contention.
205    byte_rx: mpsc::Receiver<PtyInput>,
206    reader_handle: std::thread::JoinHandle<()>,
207    /// Cached (echo, icanon) from tcgetattr; refreshed every ~250ms.
208    lflag_cache: (bool, bool),
209    lflag_last: Instant,
210    /// When we last broadcast a title update for this PTY.
211    last_title_send: Instant,
212    /// Title changed but not yet sent (debounced).
213    title_pending: bool,
214    /// The subprocess has exited but the terminal state is retained for reading.
215    exited: bool,
216    /// Exit status: WEXITSTATUS if normal exit, negative signal number if signalled,
217    /// EXIT_STATUS_UNKNOWN if not yet collected.
218    exit_status: i32,
219    /// Command used to create this PTY (None = default shell).
220    command: Option<String>,
221}
222
223impl Pty {
224    fn mark_dirty(&mut self) {
225        self.dirty = true;
226    }
227
228    fn clear_dirty(&mut self) {
229        self.dirty = false;
230    }
231}
232
233struct ClientState {
234    tx: mpsc::Sender<Vec<u8>>,
235    lead: Option<u16>,
236    subscriptions: HashSet<u16>,
237    view_sizes: HashMap<u16, (u16, u16)>,
238    scroll_offsets: HashMap<u16, usize>,
239    scroll_caches: HashMap<u16, FrameState>,
240    last_sent: HashMap<u16, FrameState>,
241    preview_next_send_at: HashMap<u16, Instant>,
242    /// EWMA RTT estimate in milliseconds.
243    rtt_ms: f32,
244    /// Minimum-path RTT estimate in milliseconds, excluding queue growth.
245    min_rtt_ms: f32,
246    /// Client's measured display refresh rate (fps), reported via C2S_DISPLAY_RATE.
247    display_fps: f32,
248    /// EWMA of delivered payload rate in bytes/sec.
249    delivery_bps: f32,
250    /// EWMA of actual ACKed goodput in bytes/sec, based on ACK cadence rather than RTT.
251    goodput_bps: f32,
252    /// EWMA of absolute goodput sample-to-sample jitter in bytes/sec.
253    goodput_jitter_bps: f32,
254    /// Decaying peak goodput jitter in bytes/sec.
255    max_goodput_jitter_bps: f32,
256    /// Last sampled ACK goodput for jitter estimation.
257    last_goodput_sample_bps: f32,
258    /// EWMA of acknowledged frame payload size in bytes.
259    avg_frame_bytes: f32,
260    /// EWMA of acknowledged lead/paced frame payload size in bytes.
261    avg_paced_frame_bytes: f32,
262    /// EWMA of acknowledged preview/unpaced frame payload size in bytes.
263    avg_preview_frame_bytes: f32,
264    /// Payload bytes currently in flight (sent, not yet ACKed).
265    inflight_bytes: usize,
266    /// Oldest in-flight frame first; ACKs arrive in order.
267    inflight_frames: VecDeque<InFlightFrame>,
268    /// Earliest time the next visual update should be sent for smooth pacing.
269    next_send_at: Instant,
270    /// Temporary additive window growth used to probe for more throughput after
271    /// a conservative backoff. Decays when queue delay grows.
272    probe_frames: f32,
273    /// Diagnostics.
274    frames_sent: u32,
275    acks_recv: u32,
276    acked_bytes_since_log: usize,
277    browser_backlog_frames: u16,
278    browser_ack_ahead_frames: u16,
279    browser_apply_ms: f32,
280    last_metrics_update: Instant,
281    last_log: Instant,
282    goodput_window_bytes: usize,
283    goodput_window_start: Instant,
284}
285
286struct InFlightFrame {
287    sent_at: Instant,
288    bytes: usize,
289    paced: bool,
290}
291
292/// Frames to keep in flight: enough to cover one RTT at the client's reported
293/// display rate. High-latency links need many frames in flight to avoid
294/// devolving into stop-and-wait.
295fn frame_window(rtt_ms: f32, display_fps: f32) -> usize {
296    let frame_ms = 1_000.0 / display_fps.max(1.0);
297    let base_frames = (rtt_ms / frame_ms).ceil().max(0.0) as usize;
298    let slack_frames = ((base_frames as f32) * 0.125).ceil() as usize + 2;
299    base_frames.saturating_add(slack_frames).max(2)
300}
301
302fn path_rtt_ms(client: &ClientState) -> f32 {
303    if client.min_rtt_ms > 0.0 {
304        client.min_rtt_ms
305    } else {
306        client.rtt_ms
307    }
308}
309
310fn display_need_bps(client: &ClientState) -> f32 {
311    client.avg_paced_frame_bytes.max(256.0) * client.display_fps.max(1.0)
312}
313
314fn effective_rtt_ms(client: &ClientState) -> f32 {
315    let path_rtt = path_rtt_ms(client);
316    let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
317    let queue_allowance = frame_ms
318        * if throughput_limited(client) {
319            4.0
320        } else {
321            12.0
322        };
323    client.rtt_ms.clamp(path_rtt, path_rtt + queue_allowance)
324}
325
326fn window_rtt_ms(client: &ClientState) -> f32 {
327    let effective = effective_rtt_ms(client);
328    if !throughput_limited(client) {
329        effective
330    } else {
331        client.rtt_ms.clamp(effective, effective * 2.0)
332    }
333}
334
335fn target_frame_window(client: &ClientState) -> usize {
336    let window_fps = if throughput_limited(client) {
337        pacing_fps(client)
338    } else {
339        browser_pacing_fps(client)
340    };
341    frame_window(window_rtt_ms(client), window_fps)
342        .saturating_add(client.probe_frames.round().max(0.0) as usize)
343}
344
345fn base_queue_ms(client: &ClientState) -> f32 {
346    let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
347    frame_ms * if throughput_limited(client) { 2.0 } else { 8.0 }
348}
349
350fn target_queue_ms(client: &ClientState) -> f32 {
351    let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
352    let probe_scale = if throughput_limited(client) {
353        0.25
354    } else {
355        1.0
356    };
357    base_queue_ms(client) + client.probe_frames.max(0.0) * frame_ms * probe_scale
358}
359
360fn browser_ready(client: &ClientState) -> bool {
361    client.browser_ack_ahead_frames <= 1
362        && client.browser_apply_ms <= 1.0
363        && !outbox_backpressured(client)
364}
365
366fn bandwidth_floor_bps(client: &ClientState) -> f32 {
367    let browser_ready = browser_ready(client);
368    let backlog_scale = match client.browser_backlog_frames {
369        0..=2 => 0.9,
370        3..=8 => 0.8,
371        _ => 0.65,
372    };
373    let penalty = client
374        .goodput_jitter_bps
375        .max(client.max_goodput_jitter_bps * 0.5)
376        .min(client.goodput_bps * if browser_ready { 0.75 } else { 0.9 });
377    let goodput_floor = (client.goodput_bps - penalty)
378        .max(client.goodput_bps * if browser_ready { 0.35 } else { 0.2 });
379    // On a browser-ready path, the per-frame delivery estimate is already
380    // end-to-end and reacts much faster than ACK-window goodput. Halving it
381    // leaves large-frame local links chronically underpaced.
382    let delivery_floor = client.delivery_bps * if browser_ready { 1.0 } else { 0.5 };
383    let recent_sample_floor = if browser_ready && client.last_goodput_sample_bps > 0.0 {
384        client.last_goodput_sample_bps * backlog_scale
385    } else {
386        0.0
387    };
388    goodput_floor.max(recent_sample_floor).max(delivery_floor)
389}
390
391fn pacing_fps(client: &ClientState) -> f32 {
392    let frame_bytes = client.avg_paced_frame_bytes.max(256.0);
393    let sustainable = bandwidth_floor_bps(client) / frame_bytes;
394    sustainable.min(browser_pacing_fps(client))
395}
396
397fn throughput_limited(client: &ClientState) -> bool {
398    let floor = bandwidth_floor_bps(client);
399    // Consider total demand: lead at cadence rate plus previews at their cap.
400    // The old check (pacing_fps < cadence * 0.9) only saw lead bandwidth,
401    // which is often tiny, so previews could starve the lead undetected.
402    let lead_bps = client.avg_paced_frame_bytes.max(256.0) * browser_pacing_fps(client);
403    let preview_bps = client.avg_preview_frame_bytes.max(256.0) * client.display_fps.max(1.0);
404    (lead_bps + preview_bps) > floor * 0.9
405}
406
407fn browser_pacing_fps(client: &ClientState) -> f32 {
408    let mut fps = client.display_fps.max(1.0);
409
410    // Backlog and ack-ahead are direct signals from the browser about
411    // whether it's keeping up.  No predictive apply-time bound — it
412    // consistently underestimates capacity and causes 30fps death spirals.
413    let backlog = client.browser_backlog_frames as f32;
414    if backlog > 4.0 {
415        fps = fps.min(fps * (4.0 / backlog));
416    }
417
418    if client.browser_ack_ahead_frames > 4 {
419        fps = fps.min(client.display_fps.max(1.0) * 0.5);
420    }
421
422    fps.max(1.0)
423}
424
425fn browser_backlog_blocked(client: &ClientState) -> bool {
426    client.browser_backlog_frames > 8
427}
428
429fn byte_budget_for(client: &ClientState, budget_ms: f32) -> usize {
430    let budget_bps = if throughput_limited(client) {
431        bandwidth_floor_bps(client)
432    } else {
433        client.goodput_bps.max(bandwidth_floor_bps(client))
434    };
435    let bytes = budget_bps * budget_ms.max(1.0) / 1_000.0;
436    bytes.ceil().max(client.avg_frame_bytes.max(256.0)) as usize
437}
438
439fn target_byte_window(client: &ClientState) -> usize {
440    let budget = byte_budget_for(client, path_rtt_ms(client) + target_queue_ms(client));
441    let frame_bytes = client.avg_paced_frame_bytes.max(256.0).ceil() as usize;
442    let target_frames = target_frame_window(client);
443    let pipeline_bytes = frame_bytes.saturating_mul(target_frames);
444    // For small pipelines (e.g. idle terminals with 1KB frames), allow the
445    // full frame window worth of bytes so we pipeline across the RTT instead
446    // of stop-and-wait.  For large pipelines (e.g. 50KB frames × 5 frames =
447    // 250KB), the budget (BDP-based) is the binding constraint; fall back to
448    // a one-frame floor so we don't pile up many RTTs worth of large frames.
449    const PIPELINE_FLOOR_LIMIT: usize = 32_768; // 32 KB
450    let floor = if pipeline_bytes <= PIPELINE_FLOOR_LIMIT {
451        pipeline_bytes
452    } else {
453        frame_bytes // one-frame floor for large pipelines
454    };
455    budget.max(floor)
456}
457
458fn send_interval(client: &ClientState) -> Duration {
459    Duration::from_secs_f64(1.0 / browser_pacing_fps(client).max(1.0) as f64)
460}
461
462fn preview_fps(client: &ClientState) -> f32 {
463    let mut fps = client.display_fps.max(1.0);
464    if client.lead.is_some() {
465        // Always budget preview bandwidth: available minus lead's share.
466        // Without this, large preview frames (e.g. 12 KB) at 30 fps consume
467        // 360 KB/s, starving the lead even when lead frames are tiny.
468        let avail = bandwidth_floor_bps(client);
469        let lead_bps = client.avg_paced_frame_bytes.max(256.0) * browser_pacing_fps(client);
470        let preview_budget = (avail - lead_bps).max(avail * 0.25).max(0.0);
471        let bw_cap = preview_budget / client.avg_preview_frame_bytes.max(256.0);
472        fps = fps.min(bw_cap.max(1.0));
473    }
474    fps.max(1.0)
475}
476
477fn preview_send_interval(client: &ClientState) -> Duration {
478    Duration::from_secs_f64(1.0 / preview_fps(client) as f64)
479}
480
481fn advance_deadline(deadline: &mut Instant, now: Instant, interval: Duration) {
482    let scheduled = deadline.checked_add(interval).unwrap_or(now + interval);
483    *deadline = if scheduled + interval < now {
484        now + interval
485    } else {
486        scheduled
487    };
488}
489
490fn should_snapshot_pty(dirty: bool, needful: bool, synced_output: bool) -> bool {
491    dirty && needful && !synced_output
492}
493
494fn enqueue_ready_frame(queue: &mut VecDeque<FrameState>, frame: FrameState) -> bool {
495    if queue.len() >= READY_FRAME_QUEUE_CAP {
496        return false;
497    }
498    queue.push_back(frame);
499    true
500}
501
502fn pty_has_visual_update(pty: &Pty) -> bool {
503    pty.dirty || !pty.ready_frames.is_empty() || !pty.byte_rx.is_empty()
504}
505
506/// Find the first `\x1b[?2026l` in `bytes`, handling sequences that span
507/// the `prefix`/`bytes` boundary. Uses SIMD-accelerated memchr for the
508/// initial ESC scan.
509fn find_sync_output_end(prefix: &[u8], bytes: &[u8]) -> Option<usize> {
510    if bytes.is_empty() {
511        return None;
512    }
513    let needle = SYNC_OUTPUT_END;
514    let nlen = needle.len();
515
516    // Check for a match straddling the prefix/bytes boundary.
517    if !prefix.is_empty() {
518        let tail = if prefix.len() >= nlen - 1 {
519            &prefix[prefix.len() - (nlen - 1)..]
520        } else {
521            prefix
522        };
523        let combined_len = tail.len() + bytes.len().min(nlen);
524        if combined_len >= nlen {
525            // Small stack buffer to check the boundary region.
526            let mut buf = [0u8; 32]; // SYNC_OUTPUT_END is 8 bytes, so 32 is plenty
527            let blen = combined_len.min(buf.len());
528            let tlen = tail.len().min(blen);
529            buf[..tlen].copy_from_slice(&tail[..tlen]);
530            let rest = (blen - tlen).min(bytes.len());
531            buf[tlen..tlen + rest].copy_from_slice(&bytes[..rest]);
532            for i in 0..=(blen.saturating_sub(nlen)) {
533                if &buf[i..i + nlen] == needle {
534                    let end_in_bytes = (i + nlen).saturating_sub(tail.len());
535                    if end_in_bytes > 0 && end_in_bytes <= bytes.len() {
536                        return Some(end_in_bytes);
537                    }
538                }
539            }
540        }
541    }
542
543    // SIMD-scan for ESC (0x1b) then verify the full sequence.
544    let mut offset = 0;
545    while let Some(pos) = memchr::memchr(0x1b, &bytes[offset..]) {
546        let abs = offset + pos;
547        if abs + nlen <= bytes.len() && &bytes[abs..abs + nlen] == needle {
548            return Some(abs + nlen);
549        }
550        offset = abs + 1;
551    }
552    None
553}
554
555fn update_sync_scan_tail(tail: &mut Vec<u8>, bytes: &[u8]) {
556    if bytes.is_empty() {
557        return;
558    }
559    tail.extend_from_slice(bytes);
560    let keep = SYNC_OUTPUT_END.len().saturating_sub(1);
561    if tail.len() > keep {
562        let drop = tail.len() - keep;
563        tail.drain(..drop);
564    }
565}
566
567fn preview_deadline(client: &ClientState, pid: u16, now: Instant) -> Instant {
568    client
569        .preview_next_send_at
570        .get(&pid)
571        .copied()
572        .unwrap_or(now)
573}
574
575fn client_has_due_preview(sess: &Session, client: &ClientState, now: Instant) -> bool {
576    if client.lead.is_none() {
577        return false;
578    }
579    client.subscriptions.iter().copied().any(|pid| {
580        Some(pid) != client.lead
581            && preview_deadline(client, pid, now) <= now
582            && sess
583                .ptys
584                .get(&pid)
585                .map(pty_has_visual_update)
586                .unwrap_or(false)
587    })
588}
589
590fn outbox_queued_frames(client: &ClientState) -> usize {
591    OUTBOX_CAPACITY.saturating_sub(client.tx.capacity())
592}
593
594fn outbox_backpressured(client: &ClientState) -> bool {
595    outbox_queued_frames(client) >= OUTBOX_SOFT_QUEUE_LIMIT_FRAMES
596}
597
598fn can_send_preview(client: &ClientState, pid: u16, now: Instant) -> bool {
599    window_open(client) && now >= preview_deadline(client, pid, now)
600}
601
602fn record_preview_send(client: &mut ClientState, pid: u16, now: Instant) {
603    let mut deadline = client
604        .preview_next_send_at
605        .get(&pid)
606        .copied()
607        .unwrap_or(now);
608    advance_deadline(&mut deadline, now, preview_send_interval(client));
609    client.preview_next_send_at.insert(pid, deadline);
610}
611
612fn window_open(client: &ClientState) -> bool {
613    !browser_backlog_blocked(client)
614        && !outbox_backpressured(client)
615        && client.inflight_frames.len() < target_frame_window(client)
616        && client.inflight_bytes < target_byte_window(client)
617}
618
619fn lead_window_open(client: &ClientState, reserve_preview_slot: bool) -> bool {
620    if !reserve_preview_slot || client.lead.is_none() {
621        return window_open(client);
622    }
623    if browser_backlog_blocked(client) || outbox_backpressured(client) {
624        return false;
625    }
626    let target_frames = target_frame_window(client);
627    let reserve_frames = PREVIEW_FRAME_RESERVE.min(target_frames.saturating_sub(1));
628    let frame_limit = target_frames.saturating_sub(reserve_frames).max(1);
629    let reserve_bytes = client.avg_preview_frame_bytes.max(256.0).ceil() as usize;
630    let byte_limit = target_byte_window(client)
631        .saturating_sub(reserve_bytes)
632        .max(client.avg_paced_frame_bytes.max(256.0).ceil() as usize);
633    client.inflight_frames.len() < frame_limit && client.inflight_bytes < byte_limit
634}
635
636fn can_send_frame(client: &ClientState, now: Instant, reserve_preview_slot: bool) -> bool {
637    lead_window_open(client, reserve_preview_slot) && now >= client.next_send_at
638}
639
640fn record_send(client: &mut ClientState, bytes: usize, now: Instant, paced: bool) {
641    client.inflight_bytes += bytes;
642    client.inflight_frames.push_back(InFlightFrame {
643        sent_at: now,
644        bytes,
645        paced,
646    });
647    if paced {
648        let interval = send_interval(client);
649        advance_deadline(&mut client.next_send_at, now, interval);
650    }
651}
652
653fn ewma_with_direction(old: f32, sample: f32, rise_alpha: f32, fall_alpha: f32) -> f32 {
654    let alpha = if sample > old { rise_alpha } else { fall_alpha };
655    old * (1.0 - alpha) + sample * alpha
656}
657
658fn window_saturated(client: &ClientState, inflight_frames: usize, inflight_bytes: usize) -> bool {
659    let target_frames = target_frame_window(client);
660    let target_bytes = target_byte_window(client);
661    inflight_frames.saturating_mul(10) >= target_frames.saturating_mul(9)
662        || inflight_bytes.saturating_mul(10) >= target_bytes.saturating_mul(9)
663}
664
665fn record_ack(client: &mut ClientState) {
666    if let Some(frame) = client.inflight_frames.pop_front() {
667        let prev_inflight_frames = client.inflight_frames.len() + 1;
668        let prev_inflight_bytes = client.inflight_bytes;
669        client.inflight_bytes = client.inflight_bytes.saturating_sub(frame.bytes);
670        client.acked_bytes_since_log = client.acked_bytes_since_log.saturating_add(frame.bytes);
671        let sample_ms = frame.sent_at.elapsed().as_secs_f32() * 1_000.0;
672        client.rtt_ms = ewma_with_direction(client.rtt_ms, sample_ms, 0.125, 0.25);
673        if client.min_rtt_ms > 0.0 {
674            // Only update downward: min_rtt tracks the unloaded path RTT and
675            // must not drift upward during congestion (queued RTT ≠ path RTT).
676            client.min_rtt_ms = client.min_rtt_ms.min(sample_ms);
677        } else {
678            client.min_rtt_ms = sample_ms;
679        }
680        client.min_rtt_ms = client.min_rtt_ms.max(0.5);
681        let sample_bps = frame.bytes as f32 / sample_ms.max(1.0e-3) * 1_000.0;
682        client.delivery_bps = ewma_with_direction(client.delivery_bps, sample_bps, 0.5, 0.125);
683        client.avg_frame_bytes =
684            ewma_with_direction(client.avg_frame_bytes, frame.bytes as f32, 0.5, 0.125);
685        if frame.paced {
686            client.avg_paced_frame_bytes =
687                ewma_with_direction(client.avg_paced_frame_bytes, frame.bytes as f32, 0.5, 0.125);
688        } else {
689            client.avg_preview_frame_bytes = ewma_with_direction(
690                client.avg_preview_frame_bytes,
691                frame.bytes as f32,
692                0.5,
693                0.125,
694            );
695        }
696        let frame_ms = 1_000.0 / browser_pacing_fps(client).max(1.0);
697        let path_rtt = path_rtt_ms(client);
698        let likely_window_limited =
699            window_saturated(client, prev_inflight_frames, prev_inflight_bytes);
700        client.goodput_window_bytes = client.goodput_window_bytes.saturating_add(frame.bytes);
701        let now = Instant::now();
702        let goodput_elapsed = now
703            .duration_since(client.goodput_window_start)
704            .as_secs_f32();
705        if goodput_elapsed >= 0.02 {
706            let sample_goodput = client.goodput_window_bytes as f32 / goodput_elapsed.max(1.0e-3);
707            if likely_window_limited || client.browser_backlog_frames > 0 {
708                let prev_goodput_sample = if client.last_goodput_sample_bps > 0.0 {
709                    client.last_goodput_sample_bps
710                } else {
711                    sample_goodput
712                };
713                let jitter_sample = (sample_goodput - prev_goodput_sample).abs();
714                client.goodput_bps =
715                    ewma_with_direction(client.goodput_bps, sample_goodput, 0.5, 0.125);
716                // Only update jitter from windows with at least 2 frames.
717                // Single-frame windows are pure measurement noise (0 or 1
718                // frame per 25 ms is a Bernoulli trial, not a congestion
719                // signal) and inflate jitter_bps, which in turn depresses
720                // bandwidth_floor_bps and causes pacing to stall.
721                let min_reliable = (client.avg_paced_frame_bytes.max(256.0) * 2.0) as usize;
722                if client.goodput_window_bytes >= min_reliable {
723                    client.goodput_jitter_bps =
724                        ewma_with_direction(client.goodput_jitter_bps, jitter_sample, 0.5, 0.125);
725                    let jitter_decay = if browser_ready(client) && sample_ms < path_rtt * 3.0 {
726                        0.90
727                    } else {
728                        0.98
729                    };
730                    client.max_goodput_jitter_bps =
731                        (client.max_goodput_jitter_bps * jitter_decay).max(jitter_sample);
732                    // Cap jitter at 45% of goodput so jitter_ratio can never
733                    // exceed 0.45 from measurement noise alone.  Real congestion
734                    // will still drive goodput_bps down and widen the window.
735                    client.max_goodput_jitter_bps =
736                        client.max_goodput_jitter_bps.min(client.goodput_bps * 0.45);
737                } else {
738                    // Thin sample: gently decay jitter rather than updating it.
739                    client.goodput_jitter_bps *= 0.9;
740                    client.max_goodput_jitter_bps *= 0.95;
741                }
742                // Sticky-high: never let last_goodput_sample_bps drop abruptly.
743                // A sudden drop (e.g. 1-frame window following a 2-frame window)
744                // inflates jitter_sample on the next cycle, collapsing probe_frames.
745                client.last_goodput_sample_bps =
746                    (client.last_goodput_sample_bps * 0.99).max(sample_goodput);
747            } else {
748                // When the path is underfilled, ACK cadence mostly measures our
749                // own pacing rather than network capacity.  Use a fall alpha
750                // proportional to estimation error: when the estimate is 10x+
751                // the sample, converge aggressively; when close, stay gentle.
752                let ratio = client.goodput_bps / sample_goodput.max(1.0);
753                let fall_alpha = if ratio > 10.0 {
754                    0.5
755                } else if ratio > 3.0 {
756                    0.25
757                } else {
758                    0.03
759                };
760                client.goodput_bps =
761                    ewma_with_direction(client.goodput_bps, sample_goodput, 0.5, fall_alpha);
762                client.goodput_jitter_bps *= 0.5;
763                client.max_goodput_jitter_bps *= 0.9;
764                client.last_goodput_sample_bps =
765                    (client.last_goodput_sample_bps * 0.99).max(sample_goodput);
766            }
767            client.goodput_window_bytes = 0;
768            client.goodput_window_start = now;
769        }
770        let queue_baseline_ms = if throughput_limited(client) {
771            window_rtt_ms(client)
772        } else {
773            path_rtt
774        };
775        let queue_delay_ms = (sample_ms - queue_baseline_ms).max(0.0);
776        let max_probe_frames = (browser_pacing_fps(client) * 0.125).max(4.0);
777        let jitter_ratio = client.max_goodput_jitter_bps / client.goodput_bps.max(1.0);
778        let low_delay_frames = if throughput_limited(client) { 2.0 } else { 8.0 };
779        let high_delay_frames = if throughput_limited(client) {
780            4.0
781        } else {
782            12.0
783        };
784        if likely_window_limited
785            && queue_delay_ms <= frame_ms * low_delay_frames
786            && jitter_ratio < 0.25
787        {
788            client.probe_frames = (client.probe_frames + 1.0).min(max_probe_frames);
789        } else if !likely_window_limited
790            && browser_ready(client)
791            && queue_delay_ms <= frame_ms * 2.0
792            && jitter_ratio < 0.25
793        {
794            client.probe_frames = (client.probe_frames + 0.25).min(max_probe_frames * 0.5);
795        } else if queue_delay_ms > frame_ms * high_delay_frames || jitter_ratio > 0.5 {
796            client.probe_frames = (client.probe_frames * 0.5).max(1.0);
797        } else if queue_delay_ms > frame_ms * 2.0 || !browser_ready(client) {
798            client.probe_frames = (client.probe_frames - 0.5).max(0.0);
799        }
800    } else {
801        client.inflight_bytes = 0;
802    }
803}
804
805fn reset_inflight(client: &mut ClientState) {
806    client.inflight_bytes = 0;
807    client.inflight_frames.clear();
808    client.next_send_at = Instant::now();
809    client.browser_backlog_frames = 0;
810    client.browser_ack_ahead_frames = 0;
811}
812
813fn is_unset_view_size(rows: u16, cols: u16) -> bool {
814    rows == 0 && cols == 0
815}
816
817fn subscribe_client_to(client: &mut ClientState, pty_id: u16) {
818    if client.subscriptions.insert(pty_id) {
819        client.last_sent.remove(&pty_id);
820        client.preview_next_send_at.remove(&pty_id);
821    }
822}
823
824fn unsubscribe_client_from(client: &mut ClientState, pty_id: u16) -> bool {
825    let removed_sub = client.subscriptions.remove(&pty_id);
826    client.last_sent.remove(&pty_id);
827    client.preview_next_send_at.remove(&pty_id);
828    client.scroll_offsets.remove(&pty_id);
829    client.scroll_caches.remove(&pty_id);
830    let removed_view = client.view_sizes.remove(&pty_id).is_some();
831    if client.lead == Some(pty_id) {
832        client.lead = None;
833    }
834    removed_sub || removed_view
835}
836
837fn update_client_scroll_state(client: &mut ClientState, pty_id: u16, next_offset: usize) -> bool {
838    let prev_offset = client.scroll_offsets.get(&pty_id).copied().unwrap_or(0);
839    if prev_offset == next_offset {
840        return false;
841    }
842
843    if prev_offset == 0 && next_offset > 0 {
844        client.scroll_caches.insert(
845            pty_id,
846            client.last_sent.get(&pty_id).cloned().unwrap_or_default(),
847        );
848    } else if prev_offset > 0 && next_offset == 0 {
849        if let Some(cache) = client.scroll_caches.remove(&pty_id) {
850            if cache.rows() > 0 && cache.cols() > 0 {
851                client.last_sent.insert(pty_id, cache);
852            } else {
853                client.last_sent.remove(&pty_id);
854            }
855        }
856    }
857
858    if next_offset > 0 {
859        client.scroll_offsets.insert(pty_id, next_offset);
860    } else {
861        client.scroll_offsets.remove(&pty_id);
862    }
863    reset_inflight(client);
864    true
865}
866
867struct Session {
868    ptys: HashMap<u16, Pty>,
869    next_client_id: u64,
870    /// Diagnostics: how many times tick() was called this second.
871    tick_fires: u32,
872    /// Diagnostics: how many ticks found the focused PTY dirty (snapshot taken).
873    tick_snaps: u32,
874    clients: HashMap<u64, ClientState>,
875}
876
877struct SearchResultRow {
878    pty_id: u16,
879    score: u32,
880    primary_source: u8,
881    matched_sources: u8,
882    context: String,
883    scroll_offset: Option<usize>,
884}
885
886struct TickOutcome {
887    did_work: bool,
888    next_deadline: Option<Instant>,
889}
890
891impl Session {
892    fn new() -> Self {
893        Self {
894            ptys: HashMap::new(),
895            next_client_id: 1,
896            clients: HashMap::new(),
897            tick_fires: 0,
898            tick_snaps: 0,
899        }
900    }
901
902    fn allocate_pty_id(&mut self) -> Option<u16> {
903        (1..=u16::MAX).find(|id| !self.ptys.contains_key(id))
904    }
905
906    fn send_to_all(&self, msg: &[u8]) {
907        for c in self.clients.values() {
908            let _ = c.tx.try_send(msg.to_vec());
909        }
910    }
911
912    fn mediated_size_for_pty(&self, pty_id: u16) -> Option<(u16, u16)> {
913        let mut min_rows: Option<u16> = None;
914        let mut min_cols: Option<u16> = None;
915        for c in self.clients.values() {
916            if let Some((r, cols)) = c.view_sizes.get(&pty_id).copied() {
917                min_rows = Some(min_rows.map_or(r, |m: u16| m.min(r)));
918                min_cols = Some(min_cols.map_or(cols, |m: u16| m.min(cols)));
919            }
920        }
921        match (min_rows, min_cols) {
922            (Some(r), Some(c)) => Some((r.max(1), c.max(1))),
923            _ => None,
924        }
925    }
926
927    fn resize_pty(&mut self, pty_id: u16, rows: u16, cols: u16) -> bool {
928        let pty = match self.ptys.get_mut(&pty_id) {
929            Some(p) => p,
930            None => return false,
931        };
932        let (cur_rows, cur_cols) = pty.driver.size();
933        if cur_rows == rows && cur_cols == cols {
934            return false;
935        }
936        pty.ready_frames.clear();
937        pty.driver.resize(rows, cols);
938        pty.mark_dirty();
939        for c in self.clients.values_mut() {
940            if c.subscriptions.contains(&pty_id) {
941                c.last_sent.remove(&pty_id);
942            }
943            if c.scroll_caches.remove(&pty_id).is_some() {
944                reset_inflight(c);
945            }
946        }
947        if !pty.exited {
948            unsafe {
949                let ws = libc::winsize {
950                    ws_row: rows,
951                    ws_col: cols,
952                    ws_xpixel: 0,
953                    ws_ypixel: 0,
954                };
955                libc::ioctl(pty.master_fd, libc::TIOCSWINSZ, &ws);
956                let mut fg_pgid: libc::pid_t = 0;
957                libc::ioctl(pty.master_fd, libc::TIOCGPGRP, &mut fg_pgid);
958                if fg_pgid > 0 {
959                    libc::kill(-fg_pgid, libc::SIGWINCH);
960                }
961                libc::kill(-pty.child_pid, libc::SIGWINCH);
962            }
963        }
964        true
965    }
966
967    fn resize_ptys_to_mediated_sizes<I>(&mut self, pty_ids: I) -> bool
968    where
969        I: IntoIterator<Item = u16>,
970    {
971        let mut changed = false;
972        let mut seen = HashSet::new();
973        for pty_id in pty_ids {
974            if !seen.insert(pty_id) {
975                continue;
976            }
977            if let Some((rows, cols)) = self.mediated_size_for_pty(pty_id) {
978                changed |= self.resize_pty(pty_id, rows, cols);
979            }
980        }
981        changed
982    }
983
984    fn pty_list_msg(&self) -> Vec<u8> {
985        let mut msg = vec![S2C_LIST];
986        let count = self.ptys.len() as u16;
987        msg.extend_from_slice(&count.to_le_bytes());
988        let mut ids: Vec<u16> = self.ptys.keys().copied().collect();
989        ids.sort();
990        for id in ids {
991            let pty = &self.ptys[&id];
992            let tag = pty.tag.as_bytes();
993            msg.extend_from_slice(&id.to_le_bytes());
994            msg.extend_from_slice(&(tag.len() as u16).to_le_bytes());
995            msg.extend_from_slice(tag);
996            let cmd = pty.command.as_deref().unwrap_or("").as_bytes();
997            msg.extend_from_slice(&(cmd.len() as u16).to_le_bytes());
998            msg.extend_from_slice(cmd);
999        }
1000        msg
1001    }
1002}
1003
1004type AppState = Arc<(Config, Mutex<Session>, PtyFds, Arc<Notify>)>;
1005
1006fn nudge_delivery(state: &AppState) {
1007    state.3.notify_one();
1008}
1009
1010fn pty_cwd(pid: libc::pid_t) -> Option<String> {
1011    #[cfg(target_os = "linux")]
1012    {
1013        std::fs::read_link(format!("/proc/{pid}/cwd"))
1014            .ok()
1015            .and_then(|p| p.into_os_string().into_string().ok())
1016    }
1017    #[cfg(target_os = "macos")]
1018    {
1019        use std::ffi::CStr;
1020        let mut buf = vec![0u8; libc::PROC_PIDPATHINFO_MAXSIZE as usize];
1021        let ret = unsafe {
1022            libc::proc_pidinfo(
1023                pid,
1024                libc::PROC_PIDVNODEPATHINFO,
1025                0,
1026                buf.as_mut_ptr() as *mut libc::c_void,
1027                std::mem::size_of::<libc::proc_vnodepathinfo>() as i32,
1028            )
1029        };
1030        if ret <= 0 {
1031            return None;
1032        }
1033        let info = unsafe { &*(buf.as_ptr() as *const libc::proc_vnodepathinfo) };
1034        let cstr =
1035            unsafe { CStr::from_ptr(info.pvi_cdir.vip_path.as_ptr() as *const libc::c_char) };
1036        cstr.to_str().ok().map(|s| s.to_owned())
1037    }
1038    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1039    {
1040        let _ = pid;
1041        None
1042    }
1043}
1044
1045/// On macOS, child processes forked from a CLI tool (as opposed to a native
1046/// .app with an NSWindow) don't inherit foreground-app scheduling: the kernel
1047/// has no window association for the PTY session, so it schedules the children
1048/// on efficiency cores with possible duty-cycling.  Explicitly requesting
1049/// QOS_CLASS_USER_INTERACTIVE restores parity with terminals like Ghostty.
1050fn set_qos_user_interactive() {
1051    #[cfg(target_os = "macos")]
1052    {
1053        const QOS_CLASS_USER_INTERACTIVE: libc::c_uint = 0x21;
1054        extern "C" {
1055            fn pthread_set_qos_class_self_np(
1056                qos_class: libc::c_uint,
1057                relative_priority: libc::c_int,
1058            ) -> libc::c_int;
1059        }
1060        unsafe {
1061            pthread_set_qos_class_self_np(QOS_CLASS_USER_INTERACTIVE, 0);
1062        }
1063    }
1064}
1065
1066#[allow(clippy::too_many_arguments)]
1067fn spawn_pty(
1068    shell: &str,
1069    rows: u16,
1070    cols: u16,
1071    id: u16,
1072    tag: &str,
1073    command: Option<&str>,
1074    argv: Option<&[&str]>,
1075    dir: Option<&str>,
1076    scrollback: usize,
1077    state: AppState,
1078) -> Option<Pty> {
1079    let mut master: libc::c_int = 0;
1080    let mut slave: libc::c_int = 0;
1081    unsafe {
1082        if libc::openpty(
1083            &mut master,
1084            &mut slave,
1085            std::ptr::null_mut(),
1086            std::ptr::null_mut(),
1087            std::ptr::null_mut(),
1088        ) != 0
1089        {
1090            eprintln!("openpty failed for pty {id}");
1091            return None;
1092        }
1093        let ws = libc::winsize {
1094            ws_row: rows,
1095            ws_col: cols,
1096            ws_xpixel: 0,
1097            ws_ypixel: 0,
1098        };
1099        libc::ioctl(master, libc::TIOCSWINSZ, &ws);
1100    }
1101
1102    let pid = unsafe { libc::fork() };
1103    if pid < 0 {
1104        eprintln!("fork failed for pty {id}");
1105        unsafe {
1106            libc::close(master);
1107            libc::close(slave);
1108        }
1109        return None;
1110    }
1111
1112    if pid == 0 {
1113        unsafe {
1114            libc::close(master);
1115            libc::setsid();
1116            libc::ioctl(slave, libc::TIOCSCTTY as _, 0);
1117            libc::dup2(slave, 0);
1118            libc::dup2(slave, 1);
1119            libc::dup2(slave, 2);
1120            if slave > 2 {
1121                libc::close(slave);
1122            }
1123        }
1124        set_qos_user_interactive();
1125        let effective_dir = dir.map(String::from);
1126        if let Some(d) = effective_dir {
1127            if let Ok(dir_c) = CString::new(d) {
1128                unsafe {
1129                    libc::chdir(dir_c.as_ptr());
1130                }
1131            }
1132        }
1133        std::env::set_var("TERM", "xterm-256color");
1134        std::env::set_var("COLORTERM", "truecolor");
1135        // Don't set COLUMNS/LINES — ncurses apps prioritize these over
1136        // TIOCGWINSZ and won't resize properly if they're set to stale values.
1137        std::env::remove_var("COLUMNS");
1138        std::env::remove_var("LINES");
1139        for (key, _) in std::env::vars() {
1140            if key.starts_with("BLIT_") && key != "BLIT_HUB" && key != "BLIT_DISPLAY_FPS" {
1141                std::env::remove_var(&key);
1142            }
1143        }
1144        let shell_flags = &state.0.shell_flags;
1145        if let Some(command) = command {
1146            let shell_c = CString::new(shell).unwrap();
1147            let command_c = CString::new(command).unwrap();
1148            let flag = CString::new(if shell_flags.is_empty() {
1149                "-c".to_owned()
1150            } else {
1151                format!("-{}c", shell_flags)
1152            })
1153            .unwrap();
1154            unsafe {
1155                let p = shell_c.as_ptr();
1156                let f = flag.as_ptr();
1157                let c = command_c.as_ptr();
1158                libc::execvp(p, [p, f, c, std::ptr::null()].as_ptr());
1159                libc::_exit(1);
1160            }
1161        }
1162        if let Some(args) = argv {
1163            if !args.is_empty() {
1164                let cargs: Vec<CString> = args.iter().map(|s| CString::new(*s).unwrap()).collect();
1165                let ptrs: Vec<*const libc::c_char> = cargs
1166                    .iter()
1167                    .map(|c| c.as_ptr())
1168                    .chain(std::iter::once(std::ptr::null()))
1169                    .collect();
1170                unsafe {
1171                    libc::execvp(ptrs[0], ptrs.as_ptr());
1172                    libc::_exit(1);
1173                }
1174            }
1175        }
1176        let shell_c = CString::new(shell).unwrap();
1177        unsafe {
1178            if shell_flags.is_empty() {
1179                let p = shell_c.as_ptr();
1180                libc::execvp(p, [p, std::ptr::null()].as_ptr());
1181            } else {
1182                let flag = CString::new(format!("-{}", shell_flags)).unwrap();
1183                let p = shell_c.as_ptr();
1184                let f = flag.as_ptr();
1185                libc::execvp(p, [p, f, std::ptr::null()].as_ptr());
1186            }
1187            libc::_exit(1);
1188        }
1189    }
1190
1191    unsafe {
1192        libc::close(slave);
1193        let flags = libc::fcntl(master, libc::F_GETFL);
1194        libc::fcntl(master, libc::F_SETFL, flags | libc::O_NONBLOCK);
1195    }
1196
1197    unsafe {
1198        libc::close(slave);
1199        let flags = libc::fcntl(master, libc::F_GETFL);
1200        libc::fcntl(master, libc::F_SETFL, flags | libc::O_NONBLOCK);
1201    }
1202
1203    state.2.write().unwrap().insert(id, master);
1204    let (byte_tx, byte_rx) = mpsc::channel(PTY_CHANNEL_CAPACITY);
1205    let reader_handle = std::thread::spawn({
1206        let notify = state.3.clone();
1207        move || pty_reader(master, byte_tx, notify)
1208    });
1209    let lflag_cache = pty_lflag(master);
1210
1211    Some(Pty {
1212        master_fd: master,
1213        child_pid: pid,
1214        driver: Box::new(AlacrittyDriver::new(rows, cols, scrollback)),
1215        tag: tag.to_owned(),
1216        dirty: true,
1217        ready_frames: VecDeque::new(),
1218        byte_rx,
1219        reader_handle,
1220        lflag_cache,
1221        lflag_last: Instant::now(),
1222        last_title_send: Instant::now(),
1223        title_pending: false,
1224        exited: false,
1225        exit_status: blit_remote::EXIT_STATUS_UNKNOWN,
1226        command: command.map(|s| s.to_owned()),
1227    })
1228}
1229
1230/// Spawn a new child process on a fresh PTY pair.
1231/// Returns (master_fd, child_pid, reader_handle, byte_rx) for swapping into an existing Pty.
1232fn respawn_child(
1233    shell: &str,
1234    rows: u16,
1235    cols: u16,
1236    pty_id: u16,
1237    command: Option<&str>,
1238    state: AppState,
1239) -> Option<(libc::c_int, libc::pid_t, std::thread::JoinHandle<()>, mpsc::Receiver<PtyInput>)> {
1240    let mut master: libc::c_int = 0;
1241    let mut slave: libc::c_int = 0;
1242    unsafe {
1243        if libc::openpty(
1244            &mut master,
1245            &mut slave,
1246            std::ptr::null_mut(),
1247            std::ptr::null_mut(),
1248            std::ptr::null_mut(),
1249        ) != 0
1250        {
1251            return None;
1252        }
1253        let ws = libc::winsize {
1254            ws_row: rows,
1255            ws_col: cols,
1256            ws_xpixel: 0,
1257            ws_ypixel: 0,
1258        };
1259        libc::ioctl(master, libc::TIOCSWINSZ, &ws);
1260    }
1261
1262    let pid = unsafe { libc::fork() };
1263    if pid < 0 {
1264        unsafe {
1265            libc::close(master);
1266            libc::close(slave);
1267        }
1268        return None;
1269    }
1270    if pid == 0 {
1271        unsafe {
1272            libc::close(master);
1273            libc::setsid();
1274            libc::ioctl(slave, libc::TIOCSCTTY as _, 0);
1275            libc::dup2(slave, 0);
1276            libc::dup2(slave, 1);
1277            libc::dup2(slave, 2);
1278            if slave > 2 {
1279                libc::close(slave);
1280            }
1281        }
1282        set_qos_user_interactive();
1283        std::env::set_var("TERM", "xterm-256color");
1284        std::env::set_var("COLORTERM", "truecolor");
1285        std::env::remove_var("COLUMNS");
1286        std::env::remove_var("LINES");
1287        for (key, _) in std::env::vars() {
1288            if key.starts_with("BLIT_") && key != "BLIT_HUB" && key != "BLIT_DISPLAY_FPS" {
1289                std::env::remove_var(&key);
1290            }
1291        }
1292        let shell_flags = &state.0.shell_flags;
1293        if let Some(cmd) = command {
1294            let shell_c = CString::new(shell).unwrap();
1295            let flag = CString::new(if shell_flags.is_empty() {
1296                "-c".to_owned()
1297            } else {
1298                format!("-{}c", shell_flags)
1299            })
1300            .unwrap();
1301            let cmd_c = CString::new(cmd).unwrap();
1302            unsafe {
1303                libc::execvp(
1304                    shell_c.as_ptr(),
1305                    [
1306                        shell_c.as_ptr(),
1307                        flag.as_ptr(),
1308                        cmd_c.as_ptr(),
1309                        std::ptr::null(),
1310                    ]
1311                    .as_ptr(),
1312                );
1313                libc::_exit(1);
1314            }
1315        }
1316        let shell_c = CString::new(shell).unwrap();
1317        unsafe {
1318            if shell_flags.is_empty() {
1319                let p = shell_c.as_ptr();
1320                libc::execvp(p, [p, std::ptr::null()].as_ptr());
1321            } else {
1322                let flag = CString::new(format!("-{}", shell_flags)).unwrap();
1323                let p = shell_c.as_ptr();
1324                let f = flag.as_ptr();
1325                libc::execvp(p, [p, f, std::ptr::null()].as_ptr());
1326            }
1327            libc::_exit(1);
1328        }
1329    }
1330
1331    unsafe {
1332        libc::close(slave);
1333        let flags = libc::fcntl(master, libc::F_GETFL);
1334        libc::fcntl(master, libc::F_SETFL, flags | libc::O_NONBLOCK);
1335    }
1336
1337    state.2.write().unwrap().insert(pty_id, master);
1338    let (byte_tx, byte_rx) = mpsc::channel(PTY_CHANNEL_CAPACITY);
1339    let reader_handle = std::thread::spawn({
1340        let notify = state.3.clone();
1341        move || pty_reader(master, byte_tx, notify)
1342    });
1343    Some((master, pid, reader_handle, byte_rx))
1344}
1345
1346fn respond_to_queries(fd: libc::c_int, data: &[u8], size: (u16, u16), cursor: (u16, u16)) {
1347    // VT420 with features matching xterm-256color capabilities.
1348    const DA1_RESPONSE: &[u8] = b"\x1b[?64;1;2;6;9;15;18;21;22c";
1349
1350    let mut i = 0;
1351    while i < data.len() {
1352        if data[i] != 0x1b || i + 2 >= data.len() || data[i + 1] != b'[' {
1353            i += 1;
1354            continue;
1355        }
1356        i += 2;
1357        let has_q = i < data.len() && data[i] == b'?';
1358        if has_q {
1359            i += 1;
1360        }
1361        let param_start = i;
1362        while i < data.len() && (data[i].is_ascii_digit() || data[i] == b';') {
1363            i += 1;
1364        }
1365        if i >= data.len() {
1366            break;
1367        }
1368        let final_byte = data[i];
1369        let params = &data[param_start..i];
1370        i += 1;
1371        if has_q {
1372            continue;
1373        }
1374        let resp: Option<String> = match final_byte {
1375            b'c' if params.is_empty() || params == b"0" => {
1376                Some(String::from_utf8_lossy(DA1_RESPONSE).into_owned())
1377            }
1378            b'n' if params == b"6" => Some(format!("\x1b[{};{}R", cursor.0 + 1, cursor.1 + 1)),
1379            b'n' if params == b"5" => Some("\x1b[0n".into()),
1380            b't' if params == b"18" => {
1381                let (rows, cols) = size;
1382                Some(format!("\x1b[8;{rows};{cols}t"))
1383            }
1384            b't' if params == b"14" => {
1385                let (rows, cols) = size;
1386                Some(format!("\x1b[4;{};{}t", rows * 16, cols * 8))
1387            }
1388            _ => None,
1389        };
1390        if let Some(r) = resp {
1391            pty_write_all(fd, r.as_bytes());
1392        }
1393    }
1394}
1395
1396fn pty_reader(
1397    fd: libc::c_int,
1398    tx: mpsc::Sender<PtyInput>,
1399    notify: Arc<Notify>,
1400) {
1401    // Use a dedicated OS thread with a plain blocking read() instead of
1402    // tokio's AsyncFd (kqueue/epoll). On macOS, registering a kqueue watcher
1403    // on the PTY master fd adds significant per-write overhead in the kernel's
1404    // TTY layer — every slave write triggers a kevent notification. A blocking
1405    // read in a dedicated thread avoids this entirely, matching what native
1406    // terminals like Ghostty do.
1407
1408    // Ensure the fd is in blocking mode.
1409    unsafe {
1410        let flags = libc::fcntl(fd, libc::F_GETFL);
1411        libc::fcntl(fd, libc::F_SETFL, flags & !libc::O_NONBLOCK);
1412    }
1413
1414    let mut buf = vec![0u8; 64 * 1024];
1415    let mut sync_scan_tail = Vec::new();
1416
1417    loop {
1418        let n = unsafe { libc::read(fd, buf.as_mut_ptr().cast(), buf.len()) };
1419        if n > 0 {
1420            let data = buf[..n as usize].to_vec();
1421            let mut remaining = data;
1422            loop {
1423                if remaining.is_empty() {
1424                    break;
1425                }
1426                if let Some(boundary) = find_sync_output_end(&sync_scan_tail, &remaining) {
1427                    let before = remaining[..boundary].to_vec();
1428                    let after = remaining[boundary..].to_vec();
1429                    update_sync_scan_tail(&mut sync_scan_tail, &before);
1430                    if tx.blocking_send(PtyInput::SyncBoundary { before, after: after.clone() }).is_err() {
1431                        return;
1432                    }
1433                    notify.notify_one();
1434                    remaining = after;
1435                } else {
1436                    update_sync_scan_tail(&mut sync_scan_tail, &remaining);
1437                    if tx.blocking_send(PtyInput::Data(remaining)).is_err() {
1438                        return;
1439                    }
1440                    notify.notify_one();
1441                    break;
1442                }
1443            }
1444        } else {
1445            let _ = tx.blocking_send(PtyInput::Eof);
1446            notify.notify_one();
1447            return;
1448        }
1449    }
1450}
1451
1452/// Split accumulated bytes at sync-output boundaries and send through the channel.
1453async fn cleanup_pty(pty_id: u16, state: &AppState) {
1454    // Remove the fd so no more writes go to the closed master.
1455    state.2.write().unwrap().remove(&pty_id);
1456    let mut sess = state.1.lock().await;
1457    if let Some(pty) = sess.ptys.get_mut(&pty_id) {
1458        if pty.exited {
1459            return;
1460        }
1461        pty.exited = true;
1462        // Reset mouse mode etc. so the client stops sending mouse events
1463        // to the now-dead shell (e.g. mpv exits without disabling ?1003).
1464        pty.driver.reset_modes();
1465        unsafe {
1466            libc::kill(pty.child_pid, libc::SIGHUP);
1467            libc::close(pty.master_fd);
1468            let mut wstatus: libc::c_int = 0;
1469            if libc::waitpid(pty.child_pid, &mut wstatus, libc::WNOHANG) > 0 {
1470                if libc::WIFEXITED(wstatus) {
1471                    pty.exit_status = libc::WEXITSTATUS(wstatus);
1472                } else if libc::WIFSIGNALED(wstatus) {
1473                    pty.exit_status = -(libc::WTERMSIG(wstatus) as i32);
1474                }
1475            }
1476        }
1477        pty.mark_dirty();
1478        let msg = blit_remote::msg_exited(pty_id, pty.exit_status);
1479        sess.send_to_all(&msg);
1480    }
1481}
1482
1483fn pty_lflag(fd: libc::c_int) -> (bool, bool) {
1484    unsafe {
1485        let mut termios: libc::termios = std::mem::zeroed();
1486        if libc::tcgetattr(fd, &mut termios) == 0 {
1487            (
1488                termios.c_lflag & libc::ECHO != 0,
1489                termios.c_lflag & libc::ICANON != 0,
1490            )
1491        } else {
1492            (false, false)
1493        }
1494    }
1495}
1496
1497fn take_snapshot(pty: &mut Pty) -> FrameState {
1498    if pty.lflag_last.elapsed() >= Duration::from_millis(250) {
1499        pty.lflag_cache = pty_lflag(pty.master_fd);
1500        pty.lflag_last = Instant::now();
1501    }
1502    let (echo, icanon) = pty.lflag_cache;
1503    pty.driver.snapshot(echo, icanon)
1504}
1505
1506fn build_scrollback_update(
1507    pty: &mut Pty,
1508    id: u16,
1509    offset: usize,
1510    prev_frame: &FrameState,
1511) -> Option<(Vec<u8>, FrameState)> {
1512    let frame = pty.driver.scrollback_frame(offset);
1513    let msg = build_update_msg(id, &frame, prev_frame);
1514    msg.map(|m| (m, frame))
1515}
1516
1517fn build_search_results_msg(request_id: u16, results: &[SearchResultRow]) -> Vec<u8> {
1518    let count = results.len().min(u16::MAX as usize);
1519    let payload_bytes: usize = results[..count]
1520        .iter()
1521        .map(|result| 14 + result.context.len().min(u16::MAX as usize))
1522        .sum();
1523    let mut msg = Vec::with_capacity(5 + payload_bytes);
1524    msg.push(S2C_SEARCH_RESULTS);
1525    msg.extend_from_slice(&request_id.to_le_bytes());
1526    msg.extend_from_slice(&(count as u16).to_le_bytes());
1527    for result in &results[..count] {
1528        msg.extend_from_slice(&result.pty_id.to_le_bytes());
1529        msg.extend_from_slice(&result.score.to_le_bytes());
1530        msg.push(result.primary_source);
1531        msg.push(result.matched_sources);
1532        let scroll_offset = result
1533            .scroll_offset
1534            .map(|offset| offset.min(u32::MAX as usize - 1) as u32)
1535            .unwrap_or(u32::MAX);
1536        msg.extend_from_slice(&scroll_offset.to_le_bytes());
1537        let context = result.context.as_bytes();
1538        let context_len = context.len().min(u16::MAX as usize);
1539        msg.extend_from_slice(&(context_len as u16).to_le_bytes());
1540        msg.extend_from_slice(&context[..context_len]);
1541    }
1542    msg
1543}
1544
1545enum SendOutcome {
1546    NoChange,
1547    Sent,
1548    Backpressured,
1549}
1550
1551fn try_send_update(
1552    client: &mut ClientState,
1553    pid: u16,
1554    current: FrameState,
1555    msg: Option<Vec<u8>>,
1556    now: Instant,
1557    paced: bool,
1558) -> SendOutcome {
1559    let Some(msg) = msg else {
1560        return SendOutcome::NoChange;
1561    };
1562    let bytes = msg.len();
1563    if client.tx.try_send(msg).is_ok() {
1564        client.last_sent.insert(pid, current);
1565        record_send(client, bytes, now, paced);
1566        client.frames_sent = client.frames_sent.wrapping_add(1);
1567        SendOutcome::Sent
1568    } else {
1569        // Outbox full — the sender can't keep up.  Advance last_sent to
1570        // the current frame so the NEXT diff is small (only changes since
1571        // now), effectively dropping this intermediate state.  Without
1572        // this, backpressure causes the tick to re-dirty the PTY, building
1573        // ever-larger diffs that make the backlog worse.
1574        client.last_sent.insert(pid, current);
1575        SendOutcome::Backpressured
1576    }
1577}
1578
1579pub fn default_socket_path() -> String {
1580    if let Ok(dir) = std::env::var("TMPDIR") {
1581        return format!("{dir}/blit.sock");
1582    }
1583    if let Ok(dir) = std::env::var("XDG_RUNTIME_DIR") {
1584        return format!("{dir}/blit.sock");
1585    }
1586    if let Ok(user) = std::env::var("USER") {
1587        return format!("/tmp/blit-{user}.sock");
1588    }
1589    "/tmp/blit.sock".into()
1590}
1591
1592
1593
1594enum RecvFdResult {
1595    Fd(RawFd),
1596    WouldBlock,
1597    Closed,
1598}
1599
1600fn recv_fd(channel: RawFd) -> RecvFdResult {
1601    unsafe {
1602        let mut buf = [0u8; 1];
1603        let mut iov = libc::iovec {
1604            iov_base: buf.as_mut_ptr() as *mut libc::c_void,
1605            iov_len: buf.len(),
1606        };
1607        let cmsg_space = libc::CMSG_SPACE(std::mem::size_of::<RawFd>() as u32) as usize;
1608        let mut cmsg_buf = vec![0u8; cmsg_space];
1609        let mut msg: libc::msghdr = std::mem::zeroed();
1610        msg.msg_iov = &mut iov;
1611        msg.msg_iovlen = 1;
1612        msg.msg_control = cmsg_buf.as_mut_ptr() as *mut libc::c_void;
1613        msg.msg_controllen = cmsg_space as _;
1614        let n = libc::recvmsg(channel, &mut msg, libc::MSG_DONTWAIT);
1615        if n < 0 {
1616            let err = std::io::Error::last_os_error();
1617            if err.kind() == std::io::ErrorKind::WouldBlock {
1618                return RecvFdResult::WouldBlock;
1619            }
1620            if err.raw_os_error() == Some(libc::EINTR) {
1621                return RecvFdResult::WouldBlock;
1622            }
1623            return RecvFdResult::Closed;
1624        }
1625        if n == 0 {
1626            return RecvFdResult::Closed;
1627        }
1628        let cmsg = libc::CMSG_FIRSTHDR(&msg);
1629        if cmsg.is_null() {
1630            return RecvFdResult::Closed;
1631        }
1632        if (*cmsg).cmsg_level == libc::SOL_SOCKET && (*cmsg).cmsg_type == libc::SCM_RIGHTS {
1633            let fd_ptr = libc::CMSG_DATA(cmsg) as *const RawFd;
1634            RecvFdResult::Fd(std::ptr::read_unaligned(fd_ptr))
1635        } else {
1636            RecvFdResult::Closed
1637        }
1638    }
1639}
1640
1641fn bind_socket(sock_path: &str) -> UnixListener {
1642    let _ = std::fs::remove_file(sock_path);
1643    let listener = UnixListener::bind(sock_path).unwrap_or_else(|e| {
1644        eprintln!("blit-server: cannot bind to {sock_path}: {e}");
1645        std::process::exit(1);
1646    });
1647    if let Err(e) = std::fs::set_permissions(sock_path, std::fs::Permissions::from_mode(0o700)) {
1648        eprintln!("blit-server: warning: cannot set socket permissions: {e}");
1649    }
1650    eprintln!("listening on {sock_path}");
1651    listener
1652}
1653
1654pub async fn run(config: Config) {
1655    let state: AppState = Arc::new((
1656        config,
1657        Mutex::new(Session::new()),
1658        Arc::new(std::sync::RwLock::new(HashMap::new())),
1659        Arc::new(Notify::new()),
1660    ));
1661
1662    let delivery_state = state.clone();
1663    tokio::spawn(async move {
1664        let mut next_deadline: Option<Instant> = None;
1665        loop {
1666            if let Some(deadline) = next_deadline {
1667                tokio::select! {
1668                    _ = delivery_state.3.notified() => {}
1669                    _ = tokio::time::sleep_until(tokio::time::Instant::from_std(deadline)) => {}
1670                }
1671            } else {
1672                delivery_state.3.notified().await;
1673            }
1674            loop {
1675                let outcome = tick(&delivery_state).await;
1676                next_deadline = outcome.next_deadline;
1677                if !outcome.did_work {
1678                    break;
1679                }
1680                tokio::task::yield_now().await;
1681            }
1682        }
1683    });
1684
1685    tokio::spawn(async {
1686        loop {
1687            tokio::time::sleep(Duration::from_secs(5)).await;
1688            unsafe { while libc::waitpid(-1, std::ptr::null_mut(), libc::WNOHANG) > 0 {} }
1689        }
1690    });
1691
1692    if let Some(channel_fd) = state.0.fd_channel {
1693        use std::os::unix::io::FromRawFd;
1694        eprintln!("accepting clients via fd-channel (fd {channel_fd})");
1695        let channel = unsafe { std::os::unix::net::UnixStream::from_raw_fd(channel_fd) };
1696        channel.set_nonblocking(true).unwrap();
1697        let async_channel = AsyncFd::new(channel).unwrap();
1698        loop {
1699            let mut guard = match async_channel.readable().await {
1700                Ok(g) => g,
1701                Err(e) => {
1702                    eprintln!("fd-channel error: {e}");
1703                    break;
1704                }
1705            };
1706            match recv_fd(channel_fd) {
1707                RecvFdResult::Fd(client_fd) => {
1708                    let std_stream =
1709                        unsafe { std::os::unix::net::UnixStream::from_raw_fd(client_fd) };
1710                    std_stream.set_nonblocking(true).unwrap();
1711                    let stream = tokio::net::UnixStream::from_std(std_stream).unwrap();
1712                    let state = state.clone();
1713                    tokio::spawn(handle_client(stream, state));
1714                    guard.retain_ready();
1715                }
1716                RecvFdResult::WouldBlock => {
1717                    guard.clear_ready();
1718                }
1719                RecvFdResult::Closed => {
1720                    break;
1721                }
1722            }
1723        }
1724        eprintln!("fd-channel closed, shutting down");
1725        return;
1726    }
1727
1728    // systemd socket activation: if LISTEN_FDS is set, use fd 3.
1729    // LISTEN_PID is checked but not required to match — some container runtimes
1730    // and service managers don't set it to the final process PID.
1731    let listener = if let Ok(fds) = std::env::var("LISTEN_FDS") {
1732        if fds.trim() == "1" {
1733            use std::os::unix::io::FromRawFd;
1734            let std_listener = unsafe { std::os::unix::net::UnixListener::from_raw_fd(3) };
1735            std_listener.set_nonblocking(true).unwrap();
1736            eprintln!("using socket activation (fd 3)");
1737            UnixListener::from_std(std_listener).unwrap()
1738        } else {
1739            eprintln!("LISTEN_FDS={fds}, expected 1; falling back to bind");
1740            bind_socket(&state.0.socket_path)
1741        }
1742    } else {
1743        bind_socket(&state.0.socket_path)
1744    };
1745
1746    loop {
1747        let (stream, _) = match listener.accept().await {
1748            Ok(conn) => conn,
1749            Err(e) => {
1750                eprintln!("accept error: {e}");
1751                tokio::time::sleep(Duration::from_millis(100)).await;
1752                continue;
1753            }
1754        };
1755        let state = state.clone();
1756        tokio::spawn(handle_client(stream, state));
1757    }
1758}
1759
1760async fn tick(state: &AppState) -> TickOutcome {
1761    let mut sess = state.1.lock().await;
1762    sess.tick_fires += 1;
1763    let mut did_work = false;
1764    let mut next_deadline: Option<Instant> = None;
1765    let now = Instant::now();
1766
1767    let max_fps = sess
1768        .clients
1769        .values()
1770        .map(browser_pacing_fps)
1771        .fold(1.0_f32, f32::max);
1772    let title_interval = Duration::from_secs_f64(1.0 / max_fps as f64);
1773    let ids: Vec<u16> = sess.ptys.keys().copied().collect();
1774    for &id in &ids {
1775        let Some(pty) = sess.ptys.get_mut(&id) else {
1776            continue;
1777        };
1778        if pty.driver.take_title_dirty() {
1779            pty.mark_dirty();
1780            pty.title_pending = true;
1781        }
1782        if pty.title_pending && now.duration_since(pty.last_title_send) >= title_interval {
1783            let msg = {
1784                let title_bytes = pty.driver.title().as_bytes();
1785                let mut msg = Vec::with_capacity(3 + title_bytes.len());
1786                msg.push(S2C_TITLE);
1787                msg.extend_from_slice(&id.to_le_bytes());
1788                msg.extend_from_slice(title_bytes);
1789                msg
1790            };
1791            pty.last_title_send = now;
1792            pty.title_pending = false;
1793            sess.send_to_all(&msg);
1794            did_work = true;
1795        }
1796    }
1797
1798    // Drain bytes from PTY reader channels. This is the only place
1799    // process() is called, so there is no contention with the readers.
1800    let mut eof_ptys: Vec<u16> = Vec::new();
1801    for &id in &ids {
1802        let Some(pty) = sess.ptys.get_mut(&id) else {
1803            continue;
1804        };
1805        while let Ok(input) = pty.byte_rx.try_recv() {
1806            match input {
1807                PtyInput::Data(data) => {
1808                    respond_to_queries(
1809                        pty.master_fd,
1810                        &data,
1811                        pty.driver.size(),
1812                        pty.driver.cursor_position(),
1813                    );
1814                    pty.driver.process(&data);
1815                    pty.mark_dirty();
1816                    did_work = true;
1817                }
1818                PtyInput::SyncBoundary { before, after } => {
1819                    if !before.is_empty() {
1820                        respond_to_queries(
1821                            pty.master_fd,
1822                            &before,
1823                            pty.driver.size(),
1824                            pty.driver.cursor_position(),
1825                        );
1826                        pty.driver.process(&before);
1827                        pty.mark_dirty();
1828                    }
1829                    if !pty.driver.synced_output() {
1830                        let frame = take_snapshot(pty);
1831                        enqueue_ready_frame(&mut pty.ready_frames, frame);
1832                        pty.clear_dirty();
1833                    }
1834                    if !after.is_empty() {
1835                        respond_to_queries(
1836                            pty.master_fd,
1837                            &after,
1838                            pty.driver.size(),
1839                            pty.driver.cursor_position(),
1840                        );
1841                        pty.driver.process(&after);
1842                        pty.mark_dirty();
1843                    }
1844                    did_work = true;
1845                }
1846                PtyInput::Eof => {
1847                    eof_ptys.push(id);
1848                }
1849            }
1850        }
1851    }
1852    // Handle EOF outside the borrow loop.
1853    drop(sess);
1854    for id in eof_ptys {
1855        tokio::time::sleep(Duration::from_millis(50)).await;
1856        cleanup_pty(id, state).await;
1857    }
1858    let mut sess = state.1.lock().await;
1859
1860    // Only snapshot PTYs that have at least one client ready to consume a fresh
1861    // frame right now. This avoids burning CPU on snapshot+diff+compress work
1862    // while the lead is merely waiting for its next pacing deadline.
1863    let needful_ptys: HashSet<u16> = sess
1864        .clients
1865        .values()
1866        .flat_map(|c| {
1867            let reserve_preview_slot = client_has_due_preview(&sess, c, now);
1868            c.subscriptions.iter().copied().filter(move |pid| {
1869                let scrolled = c.scroll_offsets.get(pid).copied().unwrap_or(0) > 0;
1870                if Some(*pid) == c.lead {
1871                    !scrolled && can_send_frame(c, now, reserve_preview_slot)
1872                } else {
1873                    !scrolled && can_send_preview(c, *pid, now)
1874                }
1875            })
1876        })
1877        .collect();
1878
1879    let mut snapshots: HashMap<u16, FrameState> = HashMap::new();
1880    for &id in &ids {
1881        let Some(pty) = sess.ptys.get_mut(&id) else {
1882            continue;
1883        };
1884        if needful_ptys.contains(&id) {
1885            if let Some(frame) = pty.ready_frames.pop_front() {
1886                snapshots.insert(id, frame);
1887                sess.tick_snaps += 1;
1888                did_work = true;
1889                continue;
1890            }
1891        }
1892        if !should_snapshot_pty(
1893            pty.dirty,
1894            needful_ptys.contains(&id),
1895            pty.driver.synced_output(),
1896        ) {
1897            continue;
1898        }
1899        // Applications that care about complete-frame boundaries should
1900        // use DEC synchronized output (?2026). Outside that bracket we
1901        // snapshot immediately instead of heuristically coalescing reads.
1902        snapshots.insert(id, take_snapshot(pty));
1903        pty.clear_dirty();
1904        sess.tick_snaps += 1;
1905        did_work = true;
1906    }
1907
1908    let client_ids: Vec<u64> = sess.clients.keys().copied().collect();
1909    for cid in client_ids {
1910        // When the pipe is idle (nothing in flight), RTT cannot be measured
1911        // and the last observed value stales.  Decay it toward min_rtt so
1912        // a stale congested RTT doesn't permanently suppress the send window
1913        // after congestion clears or traffic patterns change (e.g. switching
1914        // from a large-frame burst to idle small-frame updates).
1915        if let Some(c) = sess.clients.get_mut(&cid) {
1916            if c.inflight_bytes == 0 && c.min_rtt_ms > 0.0 && c.rtt_ms > c.min_rtt_ms {
1917                c.rtt_ms = (c.rtt_ms * 0.99 + c.min_rtt_ms * 0.01).max(c.min_rtt_ms);
1918            }
1919            // Decay stale browser metrics so a missed/delayed metrics update
1920            // can't permanently block the delivery loop.
1921            if c.last_metrics_update.elapsed() > Duration::from_secs(1) {
1922                c.browser_backlog_frames = 0;
1923                c.browser_ack_ahead_frames = 0;
1924            }
1925        }
1926        let (
1927            lead,
1928            subscriptions,
1929            scrolled_ptys,
1930            can_send_lead,
1931            lead_has_window,
1932            any_send_window,
1933            lead_deadline,
1934        ) = {
1935            let Some(c) = sess.clients.get(&cid) else {
1936                continue;
1937            };
1938            let reserve_preview_slot = client_has_due_preview(&sess, c, now);
1939            (
1940                c.lead,
1941                c.subscriptions.iter().copied().collect::<Vec<_>>(),
1942                c.scroll_offsets.iter().map(|(&k, &v)| (k, v)).collect::<Vec<_>>(),
1943                can_send_frame(c, now, reserve_preview_slot),
1944                lead_window_open(c, reserve_preview_slot),
1945                lead_window_open(c, reserve_preview_slot) || window_open(c),
1946                c.next_send_at,
1947            )
1948        };
1949
1950        if subscriptions.is_empty() {
1951            continue;
1952        }
1953
1954        // Send scrollback frames for any scrolled PTY.
1955        for &(scroll_pid, scroll_offset) in &scrolled_ptys {
1956            if scroll_offset == 0 { continue; }
1957            let is_lead = lead == Some(scroll_pid);
1958            let can_send = if is_lead { can_send_lead } else { true };
1959            if can_send {
1960                let prev_frame = {
1961                    let Some(c) = sess.clients.get(&cid) else {
1962                        continue;
1963                    };
1964                    c.scroll_caches.get(&scroll_pid).cloned().unwrap_or_default()
1965                };
1966                let outcome = if let Some(pty) = sess.ptys.get_mut(&scroll_pid) {
1967                    if let Some((msg, new_frame)) =
1968                        build_scrollback_update(pty, scroll_pid, scroll_offset, &prev_frame)
1969                    {
1970                        let Some(c) = sess.clients.get_mut(&cid) else {
1971                            break;
1972                        };
1973                        let bytes = msg.len();
1974                        if c.tx.try_send(msg).is_ok() {
1975                            c.scroll_caches.insert(scroll_pid, new_frame);
1976                            record_send(c, bytes, now, is_lead);
1977                            c.frames_sent += 1;
1978                            SendOutcome::Sent
1979                        } else {
1980                            SendOutcome::Backpressured
1981                        }
1982                    } else {
1983                        SendOutcome::NoChange
1984                    }
1985                } else {
1986                    SendOutcome::NoChange
1987                };
1988                match outcome {
1989                    SendOutcome::Sent => did_work = true,
1990                    SendOutcome::Backpressured => {
1991                        if let Some(pty) = sess.ptys.get_mut(&scroll_pid) {
1992                            pty.mark_dirty();
1993                        }
1994                    }
1995                    SendOutcome::NoChange => {}
1996                }
1997            } else if is_lead && lead_has_window {
1998                next_deadline = Some(match next_deadline {
1999                    Some(existing) => existing.min(lead_deadline),
2000                    None => lead_deadline,
2001                });
2002            }
2003        }
2004
2005        let lead_scroll_offset = lead.and_then(|pid| scrolled_ptys.iter().find(|&&(k, _)| k == pid).map(|&(_, v)| v)).unwrap_or(0);
2006
2007        if let Some(pid) = lead {
2008            if lead_scroll_offset == 0 && can_send_lead {
2009                if let Some(cur) = snapshots.get(&pid).cloned() {
2010                    let previous = sess
2011                        .clients
2012                        .get(&cid)
2013                        .and_then(|c| c.last_sent.get(&pid).cloned())
2014                        .unwrap_or_default();
2015                    drop(sess);
2016                    let msg = build_update_msg(pid, &cur, &previous);
2017                    sess = state.1.lock().await;
2018                    let Some(c) = sess.clients.get_mut(&cid) else {
2019                        continue;
2020                    };
2021                    match try_send_update(c, pid, cur, msg, now, true) {
2022                        SendOutcome::Sent => did_work = true,
2023                        SendOutcome::Backpressured => {
2024                            if let Some(pty) = sess.ptys.get_mut(&pid) {
2025                                pty.mark_dirty();
2026                            }
2027                        }
2028                        SendOutcome::NoChange => {}
2029                    }
2030                } else {
2031                    let has_pending = sess
2032                        .ptys
2033                        .get(&pid)
2034                        .map(pty_has_visual_update)
2035                        .unwrap_or(false);
2036                    let _ = has_pending;
2037                }
2038            } else {
2039                let has_pending = sess
2040                    .ptys
2041                    .get(&pid)
2042                    .map(pty_has_visual_update)
2043                    .unwrap_or(false);
2044                if has_pending && lead_has_window {
2045                    next_deadline = Some(match next_deadline {
2046                        Some(existing) => existing.min(lead_deadline),
2047                        None => lead_deadline,
2048                    });
2049                }
2050            }
2051        }
2052
2053        if !any_send_window {
2054            continue;
2055        }
2056
2057        let mut preview_ids = subscriptions;
2058        preview_ids.retain(|pid| Some(*pid) != lead);
2059        preview_ids.sort_unstable();
2060
2061        for pid in preview_ids {
2062            let (preview_can_send, preview_due_at, preview_has_window) =
2063                match sess.clients.get(&cid) {
2064                    Some(c) => (
2065                        can_send_preview(c, pid, now),
2066                        preview_deadline(c, pid, now),
2067                        window_open(c),
2068                    ),
2069                    None => (false, now, false),
2070                };
2071            if !preview_has_window {
2072                break;
2073            }
2074            if !preview_can_send {
2075                let has_pending = sess
2076                    .ptys
2077                    .get(&pid)
2078                    .map(pty_has_visual_update)
2079                    .unwrap_or(false);
2080                // Only set a deadline when the reason is *timing* (deadline
2081                // in the future), not capacity (preview window closed).
2082                // A past deadline here spins the delivery loop because
2083                // sleep_until(past) returns immediately.
2084                if has_pending && preview_due_at > now {
2085                    next_deadline = Some(match next_deadline {
2086                        Some(existing) => existing.min(preview_due_at),
2087                        None => preview_due_at,
2088                    });
2089                }
2090                continue;
2091            }
2092            let Some(cur) = snapshots.get(&pid) else {
2093                let has_pending = sess
2094                    .ptys
2095                    .get(&pid)
2096                    .map(pty_has_visual_update)
2097                    .unwrap_or(false);
2098                let _ = has_pending;
2099                continue;
2100            };
2101            let cur = cur.clone();
2102            let previous = sess
2103                .clients
2104                .get(&cid)
2105                .and_then(|c| c.last_sent.get(&pid).cloned())
2106                .unwrap_or_default();
2107            drop(sess);
2108            let msg = build_update_msg(pid, &cur, &previous);
2109            sess = state.1.lock().await;
2110            let Some(c) = sess.clients.get_mut(&cid) else {
2111                break;
2112            };
2113            match try_send_update(c, pid, cur, msg, now, false) {
2114                SendOutcome::Sent => {
2115                    record_preview_send(c, pid, now);
2116                    did_work = true;
2117                }
2118                SendOutcome::Backpressured => {
2119                    if let Some(pty) = sess.ptys.get_mut(&pid) {
2120                        pty.mark_dirty();
2121                    }
2122                    break;
2123                }
2124                SendOutcome::NoChange => {}
2125            }
2126        }
2127    }
2128
2129    TickOutcome {
2130        did_work,
2131        next_deadline,
2132    }
2133}
2134
2135async fn handle_client(stream: tokio::net::UnixStream, state: AppState) {
2136    let config = &state.0;
2137    let (mut reader, mut writer) = stream.into_split();
2138
2139    let (out_tx, mut out_rx) = mpsc::channel::<Vec<u8>>(OUTBOX_CAPACITY);
2140    let sender = tokio::spawn(async move {
2141        while let Some(msg) = out_rx.recv().await {
2142            if !write_frame(&mut writer, &msg).await {
2143                break;
2144            }
2145        }
2146    });
2147    let client_id;
2148
2149    {
2150        let mut sess = state.1.lock().await;
2151        client_id = sess.next_client_id;
2152        sess.next_client_id += 1;
2153        sess.clients.insert(
2154            client_id,
2155            ClientState {
2156                tx: out_tx,
2157                lead: None,
2158                subscriptions: HashSet::new(),
2159                view_sizes: HashMap::new(),
2160                scroll_offsets: HashMap::new(),
2161                scroll_caches: HashMap::new(),
2162                last_sent: HashMap::new(),
2163                preview_next_send_at: HashMap::new(),
2164                rtt_ms: 50.0,
2165                min_rtt_ms: 0.0,
2166                display_fps: 60.0,
2167                // Conservative seed — the rise alpha (0.5) converges up to
2168                // multi-MB/s in a handful of samples on low-latency paths. Starting
2169                // high causes catastrophic bufferbloat on slow links because
2170                // target_byte_window scales with the goodput estimate.
2171                delivery_bps: 262_144.0,
2172                goodput_bps: 262_144.0,
2173                goodput_jitter_bps: 0.0,
2174                max_goodput_jitter_bps: 0.0,
2175                last_goodput_sample_bps: 0.0,
2176                avg_frame_bytes: 1_024.0,
2177                avg_paced_frame_bytes: 1_024.0,
2178                avg_preview_frame_bytes: 1_024.0,
2179                inflight_bytes: 0,
2180                inflight_frames: VecDeque::new(),
2181                next_send_at: Instant::now(),
2182                probe_frames: 0.0,
2183                frames_sent: 0,
2184                acks_recv: 0,
2185                acked_bytes_since_log: 0,
2186                browser_backlog_frames: 0,
2187                browser_ack_ahead_frames: 0,
2188                browser_apply_ms: 0.0,
2189                last_metrics_update: Instant::now(),
2190                last_log: Instant::now(),
2191                goodput_window_bytes: 0,
2192                goodput_window_start: Instant::now(),
2193            },
2194        );
2195        if let Some(c) = sess.clients.get(&client_id) {
2196            let _ = c.tx.try_send(msg_hello(
2197                1,
2198                FEATURE_CREATE_NONCE | FEATURE_RESTART | FEATURE_RESIZE_BATCH,
2199            ));
2200        }
2201        let mut initial_msgs = Vec::new();
2202        initial_msgs.push(sess.pty_list_msg());
2203        for (&id, pty) in &sess.ptys {
2204            let title = pty.driver.title();
2205            if !title.is_empty() {
2206                let title_bytes = title.as_bytes();
2207                let mut msg = Vec::with_capacity(3 + title_bytes.len());
2208                msg.push(S2C_TITLE);
2209                msg.extend_from_slice(&id.to_le_bytes());
2210                msg.extend_from_slice(title_bytes);
2211                initial_msgs.push(msg);
2212            }
2213            if pty.exited {
2214                initial_msgs.push(blit_remote::msg_exited(id, pty.exit_status));
2215            }
2216        }
2217        initial_msgs.push(vec![S2C_READY]);
2218        let tx = sess.clients.get(&client_id).map(|c| c.tx.clone());
2219        drop(sess);
2220        if let Some(tx) = tx {
2221            for msg in initial_msgs {
2222                if tx.send(msg).await.is_err() {
2223                    break;
2224                }
2225            }
2226        }
2227    }
2228
2229    eprintln!("client connected");
2230
2231    while let Some(data) = read_frame(&mut reader).await {
2232        if data.is_empty() {
2233            continue;
2234        }
2235
2236        if data[0] == C2S_ACK {
2237            let mut sess = state.1.lock().await;
2238            let (
2239                do_log,
2240                frames_sent,
2241                acks_recv,
2242                rtt_ms,
2243                min_rtt_ms,
2244                eff_rtt_ms,
2245                inflight_bytes,
2246                delivery_bps,
2247                goodput_ewma_bps,
2248                goodput_jitter_bps,
2249                max_goodput_jitter_bps,
2250                avg_frame_bytes,
2251                avg_paced_frame_bytes,
2252                avg_preview_frame_bytes,
2253                display_fps,
2254                paced_fps,
2255                display_need_bps,
2256                probe_frames,
2257                goodput_bps,
2258                window_frames,
2259                window_bytes,
2260                outbox_frames,
2261                browser_backlog_frames,
2262                browser_ack_ahead_frames,
2263                browser_apply_ms,
2264            ) = {
2265                let Some(c) = sess.clients.get_mut(&client_id) else {
2266                    continue;
2267                };
2268                c.acks_recv += 1;
2269                record_ack(c);
2270                let do_log = c.last_log.elapsed().as_secs_f32() >= 1.0;
2271                let log_elapsed = c.last_log.elapsed().as_secs_f32().max(1.0e-3);
2272                let paced_fps = pacing_fps(c);
2273                let display_need_bps = display_need_bps(c);
2274                let out = (
2275                    do_log,
2276                    c.frames_sent,
2277                    c.acks_recv,
2278                    c.rtt_ms,
2279                    path_rtt_ms(c),
2280                    window_rtt_ms(c),
2281                    c.inflight_bytes,
2282                    c.delivery_bps,
2283                    c.goodput_bps,
2284                    c.goodput_jitter_bps,
2285                    c.max_goodput_jitter_bps,
2286                    c.avg_frame_bytes,
2287                    c.avg_paced_frame_bytes,
2288                    c.avg_preview_frame_bytes,
2289                    c.display_fps,
2290                    paced_fps,
2291                    display_need_bps,
2292                    c.probe_frames,
2293                    c.acked_bytes_since_log as f32 / log_elapsed,
2294                    target_frame_window(c),
2295                    target_byte_window(c),
2296                    outbox_queued_frames(c),
2297                    c.browser_backlog_frames,
2298                    c.browser_ack_ahead_frames,
2299                    c.browser_apply_ms,
2300                );
2301                if do_log {
2302                    c.frames_sent = 0;
2303                    c.acks_recv = 0;
2304                    c.acked_bytes_since_log = 0;
2305                    c.last_log = Instant::now();
2306                }
2307                out
2308            };
2309            if do_log {
2310                eprintln!(
2311                    "client {client_id}: sent={frames_sent} acks={acks_recv} rtt={rtt_ms:.0}ms min_rtt={min_rtt_ms:.0}ms eff_rtt={eff_rtt_ms:.0}ms window={window_frames}f/{window_bytes}B probe={probe_frames:.0}f inflight={inflight_bytes}B outbox={outbox_frames}f goodput={goodput_bps:.0}B/s goodput_ewma={goodput_ewma_bps:.0}B/s jitter={goodput_jitter_bps:.0}/{max_goodput_jitter_bps:.0}B/s rate={delivery_bps:.0}B/s avg_frame={avg_frame_bytes:.0}B lead_frame={avg_paced_frame_bytes:.0}B preview_frame={avg_preview_frame_bytes:.0}B need={display_need_bps:.0}B/s display_fps={display_fps:.0} paced_fps={paced_fps:.0} backlog={browser_backlog_frames} ack_ahead={browser_ack_ahead_frames} apply={browser_apply_ms:.1}ms | tick_fires={} tick_snaps={}",
2312                    sess.tick_fires, sess.tick_snaps,
2313                );
2314                sess.tick_fires = 0;
2315                sess.tick_snaps = 0;
2316            }
2317            nudge_delivery(&state);
2318            continue;
2319        }
2320
2321        if data[0] == C2S_DISPLAY_RATE && data.len() >= 3 {
2322            let fps = u16::from_le_bytes([data[1], data[2]]) as f32;
2323            if fps > 0.0 {
2324                let mut sess = state.1.lock().await;
2325                if let Some(c) = sess.clients.get_mut(&client_id) {
2326                    c.display_fps = fps;
2327                }
2328            }
2329            nudge_delivery(&state);
2330            continue;
2331        }
2332
2333        if data[0] == C2S_CLIENT_METRICS && data.len() >= 7 {
2334            let backlog_frames = u16::from_le_bytes([data[1], data[2]]);
2335            let ack_ahead_frames = u16::from_le_bytes([data[3], data[4]]);
2336            let apply_ms = u16::from_le_bytes([data[5], data[6]]) as f32 * 0.1;
2337            let mut sess = state.1.lock().await;
2338            if let Some(c) = sess.clients.get_mut(&client_id) {
2339                c.browser_backlog_frames = backlog_frames;
2340                c.browser_ack_ahead_frames = ack_ahead_frames;
2341                c.browser_apply_ms = apply_ms;
2342                c.last_metrics_update = Instant::now();
2343            }
2344            nudge_delivery(&state);
2345            continue;
2346        }
2347
2348        // Server-side mouse: client sends structured mouse data, server generates
2349        // the correct escape sequence using the terminal's current mouse mode/encoding.
2350        if data[0] == C2S_MOUSE && data.len() >= 9 {
2351            let pid = u16::from_le_bytes([data[1], data[2]]);
2352            let type_ = data[3];
2353            let button = data[4];
2354            let col = u16::from_le_bytes([data[5], data[6]]);
2355            let row = u16::from_le_bytes([data[7], data[8]]);
2356            let sess = state.1.lock().await;
2357            if let Some(pty) = sess.ptys.get(&pid) {
2358                let (echo, icanon) = pty.lflag_cache;
2359                if let Some(seq) = pty
2360                    .driver
2361                    .mouse_event(type_, button, col, row, echo, icanon)
2362                {
2363                    if let Some(&fd) = state.2.read().unwrap().get(&pid) {
2364                        pty_write_all(fd, &seq);
2365                    }
2366                }
2367            }
2368            continue;
2369        }
2370
2371        if data[0] == C2S_INPUT && data.len() >= 3 {
2372            let pid = u16::from_le_bytes([data[1], data[2]]);
2373            let mut need_nudge = false;
2374            {
2375                let mut sess = state.1.lock().await;
2376                if let Some(c) = sess.clients.get_mut(&client_id) {
2377                    if update_client_scroll_state(c, pid, 0) {
2378                        if let Some(pty) = sess.ptys.get_mut(&pid) {
2379                            pty.mark_dirty();
2380                            need_nudge = true;
2381                        }
2382                    }
2383                }
2384            }
2385            if need_nudge {
2386                nudge_delivery(&state);
2387            }
2388            if let Some(&fd) = state.2.read().unwrap().get(&pid) {
2389                pty_write_all(fd, &data[3..]);
2390            }
2391            continue;
2392        }
2393
2394        if data[0] == C2S_SEARCH && data.len() >= 3 {
2395            let request_id = u16::from_le_bytes([data[1], data[2]]);
2396            let query = std::str::from_utf8(&data[3..]).unwrap_or("").trim();
2397            let mut sess = state.1.lock().await;
2398            let lead = sess.clients.get(&client_id).and_then(|c| c.lead);
2399            let mut ranked: Vec<SearchResultRow> = if query.is_empty() {
2400                Vec::new()
2401            } else {
2402                sess.ptys
2403                    .iter()
2404                    .filter_map(|(&pty_id, pty)| {
2405                        pty.driver
2406                            .search_result(query)
2407                            .map(|result| SearchResultRow {
2408                                pty_id,
2409                                score: result.score,
2410                                primary_source: result.primary_source,
2411                                matched_sources: result.matched_sources,
2412                                context: result.context,
2413                                scroll_offset: result.scroll_offset,
2414                            })
2415                    })
2416                    .collect()
2417            };
2418            ranked.sort_by(|a, b| {
2419                b.score
2420                    .cmp(&a.score)
2421                    .then_with(|| (Some(b.pty_id) == lead).cmp(&(Some(a.pty_id) == lead)))
2422                    .then_with(|| a.pty_id.cmp(&b.pty_id))
2423            });
2424            if let Some(client) = sess.clients.get_mut(&client_id) {
2425                let _ = client
2426                    .tx
2427                    .try_send(build_search_results_msg(request_id, &ranked));
2428            }
2429            continue;
2430        }
2431
2432        let mut sess = state.1.lock().await;
2433        let mut need_nudge = false;
2434        match data[0] {
2435            C2S_SCROLL if data.len() >= 7 => {
2436                let pid = u16::from_le_bytes([data[1], data[2]]);
2437                let offset = u32::from_le_bytes([data[3], data[4], data[5], data[6]]) as usize;
2438                if sess.ptys.contains_key(&pid) {
2439                    if let Some(c) = sess.clients.get_mut(&client_id) {
2440                        update_client_scroll_state(c, pid, offset);
2441                    }
2442                    if let Some(pty) = sess.ptys.get_mut(&pid) {
2443                        pty.mark_dirty();
2444                        need_nudge = true;
2445                    }
2446                }
2447            }
2448            C2S_RESIZE if data.len() >= 7 => {
2449                let entries = data[1..].chunks_exact(6);
2450                if !entries.remainder().is_empty() {
2451                    continue;
2452                }
2453                let mut touched = Vec::new();
2454                for entry in entries {
2455                    let pid = u16::from_le_bytes([entry[0], entry[1]]);
2456                    if !sess.ptys.contains_key(&pid) {
2457                        continue;
2458                    }
2459                    let rows = u16::from_le_bytes([entry[2], entry[3]]);
2460                    let cols = u16::from_le_bytes([entry[4], entry[5]]);
2461                    if let Some(c) = sess.clients.get_mut(&client_id) {
2462                        if is_unset_view_size(rows, cols) {
2463                            if c.view_sizes.remove(&pid).is_some() {
2464                                touched.push(pid);
2465                            }
2466                        } else if rows == 0 || cols == 0 {
2467                            continue;
2468                        } else {
2469                            c.view_sizes.insert(pid, (rows, cols));
2470                            touched.push(pid);
2471                        }
2472                    }
2473                }
2474                if sess.resize_ptys_to_mediated_sizes(touched) {
2475                    need_nudge = true;
2476                }
2477            }
2478            C2S_CREATE => {
2479                // Format: [opcode][rows:2][cols:2][tag_len:2][tag:N][command...]
2480                let (rows, cols) = if data.len() >= 5 {
2481                    (
2482                        u16::from_le_bytes([data[1], data[2]]),
2483                        u16::from_le_bytes([data[3], data[4]]),
2484                    )
2485                } else {
2486                    (24, 80)
2487                };
2488                let tag_len = if data.len() >= 7 {
2489                    u16::from_le_bytes([data[5], data[6]]) as usize
2490                } else {
2491                    0
2492                };
2493                let tag = if data.len() >= 7 + tag_len {
2494                    std::str::from_utf8(&data[7..7 + tag_len]).unwrap_or_default()
2495                } else {
2496                    ""
2497                };
2498                let cmd_start = 7 + tag_len;
2499                let dir: Option<String> = None;
2500                let create_payload = data
2501                    .get(cmd_start..)
2502                    .and_then(|bytes| std::str::from_utf8(bytes).ok());
2503                let command = create_payload
2504                    .filter(|payload| !payload.contains('\0'))
2505                    .map(str::trim)
2506                    .filter(|payload| !payload.is_empty());
2507                let argv: Option<Vec<&str>> = create_payload
2508                    .filter(|payload| payload.contains('\0'))
2509                    .map(|payload| {
2510                        payload
2511                            .split('\0')
2512                            .filter(|arg| !arg.is_empty())
2513                            .collect::<Vec<_>>()
2514                    })
2515                    .filter(|args| !args.is_empty());
2516                let Some(id) = sess.allocate_pty_id() else {
2517                    continue;
2518                };
2519                if let Some(pty) = spawn_pty(
2520                    &config.shell,
2521                    rows,
2522                    cols,
2523                    id,
2524                    tag,
2525                    command,
2526                    argv.as_deref(),
2527                    dir.as_deref(),
2528                    config.scrollback,
2529                    state.clone(),
2530                ) {
2531                    let mut msg = Vec::with_capacity(3 + pty.tag.len());
2532                    msg.push(S2C_CREATED);
2533                    msg.extend_from_slice(&id.to_le_bytes());
2534                    msg.extend_from_slice(pty.tag.as_bytes());
2535                    sess.ptys.insert(id, pty);
2536                    if let Some(c) = sess.clients.get_mut(&client_id) {
2537                        c.lead = Some(id);
2538                        c.view_sizes.insert(id, (rows, cols));
2539                        subscribe_client_to(c, id);
2540                        // Per-PTY scroll: no blanket reset needed.
2541                        reset_inflight(c);
2542                    }
2543                    sess.send_to_all(&msg);
2544                    need_nudge = true;
2545                }
2546            }
2547            C2S_CREATE_N => {
2548                // Format: [opcode][nonce:2][rows:2][cols:2][tag_len:2][tag:N][command...]
2549                let nonce = if data.len() >= 3 {
2550                    u16::from_le_bytes([data[1], data[2]])
2551                } else {
2552                    0
2553                };
2554                let (rows, cols) = if data.len() >= 7 {
2555                    (
2556                        u16::from_le_bytes([data[3], data[4]]),
2557                        u16::from_le_bytes([data[5], data[6]]),
2558                    )
2559                } else {
2560                    (24, 80)
2561                };
2562                let tag_len = if data.len() >= 9 {
2563                    u16::from_le_bytes([data[7], data[8]]) as usize
2564                } else {
2565                    0
2566                };
2567                let tag = if data.len() >= 9 + tag_len {
2568                    std::str::from_utf8(&data[9..9 + tag_len]).unwrap_or_default()
2569                } else {
2570                    ""
2571                };
2572                let cmd_start = 9 + tag_len;
2573                let dir: Option<String> = None;
2574                let create_payload = data
2575                    .get(cmd_start..)
2576                    .and_then(|bytes| std::str::from_utf8(bytes).ok());
2577                let command = create_payload
2578                    .filter(|payload| !payload.contains('\0'))
2579                    .map(str::trim)
2580                    .filter(|payload| !payload.is_empty());
2581                let argv: Option<Vec<&str>> = create_payload
2582                    .filter(|payload| payload.contains('\0'))
2583                    .map(|payload| {
2584                        payload
2585                            .split('\0')
2586                            .filter(|arg| !arg.is_empty())
2587                            .collect::<Vec<_>>()
2588                    })
2589                    .filter(|args| !args.is_empty());
2590                let Some(id) = sess.allocate_pty_id() else {
2591                    continue;
2592                };
2593                if let Some(pty) = spawn_pty(
2594                    &config.shell,
2595                    rows,
2596                    cols,
2597                    id,
2598                    tag,
2599                    command,
2600                    argv.as_deref(),
2601                    dir.as_deref(),
2602                    config.scrollback,
2603                    state.clone(),
2604                ) {
2605                    let tag_bytes = pty.tag.as_bytes();
2606                    let mut nonce_msg = Vec::with_capacity(5 + tag_bytes.len());
2607                    nonce_msg.push(S2C_CREATED_N);
2608                    nonce_msg.extend_from_slice(&nonce.to_le_bytes());
2609                    nonce_msg.extend_from_slice(&id.to_le_bytes());
2610                    nonce_msg.extend_from_slice(tag_bytes);
2611                    let mut broadcast_msg = Vec::with_capacity(3 + tag_bytes.len());
2612                    broadcast_msg.push(S2C_CREATED);
2613                    broadcast_msg.extend_from_slice(&id.to_le_bytes());
2614                    broadcast_msg.extend_from_slice(tag_bytes);
2615                    sess.ptys.insert(id, pty);
2616                    if let Some(c) = sess.clients.get_mut(&client_id) {
2617                        c.lead = Some(id);
2618                        c.view_sizes.insert(id, (rows, cols));
2619                        subscribe_client_to(c, id);
2620                        // Per-PTY scroll: no blanket reset needed.
2621                        reset_inflight(c);
2622                        let _ = c.tx.try_send(nonce_msg);
2623                    }
2624                    for (&cid, c) in sess.clients.iter() {
2625                        if cid != client_id {
2626                            let _ = c.tx.try_send(broadcast_msg.clone());
2627                        }
2628                    }
2629                    need_nudge = true;
2630                }
2631            }
2632            C2S_CREATE_AT => {
2633                // Format: [opcode][rows:2][cols:2][tag_len:2][tag:N][src_pty_id:2]
2634                let (rows, cols) = if data.len() >= 5 {
2635                    (
2636                        u16::from_le_bytes([data[1], data[2]]),
2637                        u16::from_le_bytes([data[3], data[4]]),
2638                    )
2639                } else {
2640                    (24, 80)
2641                };
2642                let tag_len = if data.len() >= 7 {
2643                    u16::from_le_bytes([data[5], data[6]]) as usize
2644                } else {
2645                    0
2646                };
2647                let tag = if data.len() >= 7 + tag_len {
2648                    std::str::from_utf8(&data[7..7 + tag_len]).unwrap_or_default()
2649                } else {
2650                    ""
2651                };
2652                let src_start = 7 + tag_len;
2653                let dir = if data.len() >= src_start + 2 {
2654                    let src_id = u16::from_le_bytes([data[src_start], data[src_start + 1]]);
2655                    sess.ptys.get(&src_id).and_then(|p| pty_cwd(p.child_pid))
2656                } else {
2657                    None
2658                };
2659                let Some(id) = sess.allocate_pty_id() else {
2660                    continue;
2661                };
2662                if let Some(pty) = spawn_pty(
2663                    &config.shell,
2664                    rows,
2665                    cols,
2666                    id,
2667                    tag,
2668                    None,
2669                    None,
2670                    dir.as_deref(),
2671                    config.scrollback,
2672                    state.clone(),
2673                ) {
2674                    let mut msg = Vec::with_capacity(3 + pty.tag.len());
2675                    msg.push(S2C_CREATED);
2676                    msg.extend_from_slice(&id.to_le_bytes());
2677                    msg.extend_from_slice(pty.tag.as_bytes());
2678                    sess.ptys.insert(id, pty);
2679                    if let Some(c) = sess.clients.get_mut(&client_id) {
2680                        c.lead = Some(id);
2681                        c.view_sizes.insert(id, (rows, cols));
2682                        subscribe_client_to(c, id);
2683                        // Per-PTY scroll: no blanket reset needed.
2684                        reset_inflight(c);
2685                    }
2686                    sess.send_to_all(&msg);
2687                    need_nudge = true;
2688                }
2689            }
2690            C2S_CREATE2 => {
2691                // Generic create: [0x18][nonce:2][rows:2][cols:2][features:1][tag_len:2][tag:N][...fields]
2692                if data.len() < 10 {
2693                    continue;
2694                }
2695                let nonce = u16::from_le_bytes([data[1], data[2]]);
2696                let rows = u16::from_le_bytes([data[3], data[4]]);
2697                let cols = u16::from_le_bytes([data[5], data[6]]);
2698                let features = data[7];
2699                let tag_len = u16::from_le_bytes([data[8], data[9]]) as usize;
2700                let tag = if data.len() >= 10 + tag_len {
2701                    std::str::from_utf8(&data[10..10 + tag_len]).unwrap_or_default()
2702                } else {
2703                    ""
2704                };
2705                let mut cursor = 10 + tag_len;
2706                let dir = if features & CREATE2_HAS_SRC_PTY != 0 && data.len() >= cursor + 2 {
2707                    let src_id = u16::from_le_bytes([data[cursor], data[cursor + 1]]);
2708                    cursor += 2;
2709                    sess.ptys.get(&src_id).and_then(|p| pty_cwd(p.child_pid))
2710                } else {
2711                    None
2712                };
2713                let create_payload = if features & CREATE2_HAS_COMMAND != 0 {
2714                    data.get(cursor..).and_then(|b| std::str::from_utf8(b).ok())
2715                } else {
2716                    None
2717                };
2718                let command = create_payload
2719                    .filter(|p| !p.contains('\0'))
2720                    .map(str::trim)
2721                    .filter(|p| !p.is_empty());
2722                let argv: Option<Vec<&str>> = create_payload
2723                    .filter(|p| p.contains('\0'))
2724                    .map(|p| p.split('\0').filter(|a| !a.is_empty()).collect::<Vec<_>>())
2725                    .filter(|a| !a.is_empty());
2726                let Some(id) = sess.allocate_pty_id() else {
2727                    continue;
2728                };
2729                if let Some(pty) = spawn_pty(
2730                    &config.shell,
2731                    rows,
2732                    cols,
2733                    id,
2734                    tag,
2735                    command,
2736                    argv.as_deref(),
2737                    dir.as_deref(),
2738                    config.scrollback,
2739                    state.clone(),
2740                ) {
2741                    let tag_bytes = pty.tag.as_bytes();
2742                    let mut nonce_msg = Vec::with_capacity(5 + tag_bytes.len());
2743                    nonce_msg.push(S2C_CREATED_N);
2744                    nonce_msg.extend_from_slice(&nonce.to_le_bytes());
2745                    nonce_msg.extend_from_slice(&id.to_le_bytes());
2746                    nonce_msg.extend_from_slice(tag_bytes);
2747                    let mut broadcast_msg = Vec::with_capacity(3 + tag_bytes.len());
2748                    broadcast_msg.push(S2C_CREATED);
2749                    broadcast_msg.extend_from_slice(&id.to_le_bytes());
2750                    broadcast_msg.extend_from_slice(tag_bytes);
2751                    sess.ptys.insert(id, pty);
2752                    if let Some(c) = sess.clients.get_mut(&client_id) {
2753                        c.lead = Some(id);
2754                        c.view_sizes.insert(id, (rows, cols));
2755                        subscribe_client_to(c, id);
2756                        // Per-PTY scroll: no blanket reset needed.
2757                        reset_inflight(c);
2758                        let _ = c.tx.try_send(nonce_msg);
2759                    }
2760                    for (&cid, c) in sess.clients.iter() {
2761                        if cid != client_id {
2762                            let _ = c.tx.try_send(broadcast_msg.clone());
2763                        }
2764                    }
2765                    need_nudge = true;
2766                }
2767            }
2768            C2S_FOCUS if data.len() >= 3 => {
2769                let pid = u16::from_le_bytes([data[1], data[2]]);
2770                if sess.ptys.contains_key(&pid) {
2771                    let old_pid = sess.clients.get(&client_id).and_then(|c| c.lead);
2772                    if let Some(c) = sess.clients.get_mut(&client_id) {
2773                        c.lead = Some(pid);
2774                        subscribe_client_to(c, pid);
2775                        if old_pid == Some(pid) {
2776                            update_client_scroll_state(c, pid, 0);
2777                        } else {
2778                            reset_inflight(c);
2779                        }
2780                    }
2781                    if let Some(pty) = sess.ptys.get_mut(&pid) {
2782                        pty.mark_dirty();
2783                        need_nudge = true;
2784                    }
2785                }
2786            }
2787            C2S_SUBSCRIBE if data.len() >= 3 => {
2788                let pid = u16::from_le_bytes([data[1], data[2]]);
2789                if sess.ptys.contains_key(&pid) {
2790                    if let Some(c) = sess.clients.get_mut(&client_id) {
2791                        subscribe_client_to(c, pid);
2792                    }
2793                    if let Some(pty) = sess.ptys.get_mut(&pid) {
2794                        pty.mark_dirty();
2795                    }
2796                    need_nudge = true;
2797                }
2798            }
2799            C2S_UNSUBSCRIBE if data.len() >= 3 => {
2800                let pid = u16::from_le_bytes([data[1], data[2]]);
2801                if sess.ptys.contains_key(&pid) {
2802                    let mut touched = Vec::new();
2803                    if let Some(c) = sess.clients.get_mut(&client_id) {
2804                        if unsubscribe_client_from(c, pid) {
2805                            touched.push(pid);
2806                        }
2807                        reset_inflight(c);
2808                    }
2809                    if sess.resize_ptys_to_mediated_sizes(touched) {
2810                        need_nudge = true;
2811                    }
2812                }
2813            }
2814            C2S_RESTART if data.len() >= 3 => {
2815                let pid = u16::from_le_bytes([data[1], data[2]]);
2816                let restart_info = sess
2817                    .ptys
2818                    .get(&pid)
2819                    .filter(|p| p.exited)
2820                    .map(|p| (p.driver.size(), p.command.clone(), p.tag.clone()));
2821                if let Some(((rows, cols), command, tag)) = restart_info {
2822                    if let Some((master, child, reader, byte_rx)) = respawn_child(
2823                        &state.0.shell,
2824                        rows,
2825                        cols,
2826                        pid,
2827                        command.as_deref(),
2828                        state.clone(),
2829                    ) {
2830                        let Some(pty) = sess.ptys.get_mut(&pid) else {
2831                            break;
2832                        };
2833                        pty.master_fd = master;
2834                        pty.child_pid = child;
2835                        pty.reader_handle = reader;
2836                        pty.byte_rx = byte_rx;
2837                        pty.exited = false;
2838                        pty.exit_status = blit_remote::EXIT_STATUS_UNKNOWN;
2839                        pty.lflag_cache = pty_lflag(master);
2840                        pty.lflag_last = Instant::now();
2841                        pty.mark_dirty();
2842                        if let Some(c) = sess.clients.get_mut(&client_id) {
2843                            c.lead = Some(pid);
2844                            subscribe_client_to(c, pid);
2845                            update_client_scroll_state(c, pid, 0);
2846                            reset_inflight(c);
2847                        }
2848                        let mut msg = Vec::with_capacity(3 + tag.len());
2849                        msg.push(S2C_CREATED);
2850                        msg.extend_from_slice(&pid.to_le_bytes());
2851                        msg.extend_from_slice(tag.as_bytes());
2852                        sess.send_to_all(&msg);
2853                        need_nudge = true;
2854                    }
2855                }
2856            }
2857            C2S_READ if data.len() >= 13 => {
2858                let nonce = u16::from_le_bytes([data[1], data[2]]);
2859                let pid = u16::from_le_bytes([data[3], data[4]]);
2860                let req_offset = u32::from_le_bytes([data[5], data[6], data[7], data[8]]) as usize;
2861                let req_limit =
2862                    u32::from_le_bytes([data[9], data[10], data[11], data[12]]) as usize;
2863                let flags = data.get(13).copied().unwrap_or(0);
2864                let ansi = flags & READ_ANSI != 0;
2865                let tail = flags & READ_TAIL != 0;
2866
2867                if let Some(pty) = sess.ptys.get_mut(&pid) {
2868                    let (rows, _cols) = pty.driver.size();
2869                    let viewport = take_snapshot(pty);
2870                    let scrollback_lines = viewport.scrollback_lines() as usize;
2871                    let total_lines = scrollback_lines + rows as usize;
2872
2873                    let extract = |f: &FrameState| -> String {
2874                        if ansi {
2875                            f.get_ansi_text()
2876                        } else {
2877                            f.get_all_text()
2878                        }
2879                    };
2880
2881                    let mut all_lines: Vec<String> = Vec::new();
2882
2883                    let mut scroll_offset = scrollback_lines;
2884                    while scroll_offset > 0 {
2885                        let frame = pty.driver.scrollback_frame(scroll_offset);
2886                        let page = extract(&frame);
2887                        let page_lines: Vec<&str> = page.lines().collect();
2888                        let take = if scroll_offset < rows as usize {
2889                            scroll_offset.min(page_lines.len())
2890                        } else {
2891                            page_lines.len()
2892                        };
2893                        for line in &page_lines[..take] {
2894                            all_lines.push(line.to_string());
2895                        }
2896                        if scroll_offset <= rows as usize {
2897                            break;
2898                        }
2899                        scroll_offset = scroll_offset.saturating_sub(rows as usize);
2900                    }
2901
2902                    for line in extract(&viewport).lines() {
2903                        all_lines.push(line.to_string());
2904                    }
2905
2906                    let (start, end) = if tail {
2907                        let end = all_lines.len().saturating_sub(req_offset);
2908                        let start = if req_limit == 0 {
2909                            0
2910                        } else {
2911                            end.saturating_sub(req_limit)
2912                        };
2913                        (start, end)
2914                    } else {
2915                        let start = req_offset.min(all_lines.len());
2916                        let end = if req_limit == 0 {
2917                            all_lines.len()
2918                        } else {
2919                            (start + req_limit).min(all_lines.len())
2920                        };
2921                        (start, end)
2922                    };
2923                    let text = all_lines[start..end].join("\n");
2924
2925                    let mut msg = Vec::with_capacity(13 + text.len());
2926                    msg.push(S2C_TEXT);
2927                    msg.extend_from_slice(&nonce.to_le_bytes());
2928                    msg.extend_from_slice(&pid.to_le_bytes());
2929                    msg.extend_from_slice(&(total_lines as u32).to_le_bytes());
2930                    msg.extend_from_slice(&(start as u32).to_le_bytes());
2931                    msg.extend_from_slice(text.as_bytes());
2932                    if let Some(client) = sess.clients.get(&client_id) {
2933                        let _ = client.tx.try_send(msg);
2934                    }
2935                }
2936            }
2937            C2S_KILL if data.len() >= 7 => {
2938                let pid = u16::from_le_bytes([data[1], data[2]]);
2939                let signal = i32::from_le_bytes([data[3], data[4], data[5], data[6]]);
2940                if let Some(pty) = sess.ptys.get(&pid) {
2941                    if !pty.exited {
2942                        unsafe {
2943                            libc::kill(pty.child_pid, signal);
2944                        }
2945                    }
2946                }
2947            }
2948            C2S_CLOSE if data.len() >= 3 => {
2949                let pid = u16::from_le_bytes([data[1], data[2]]);
2950                if let Some(pty) = sess.ptys.remove(&pid) {
2951                    if !pty.exited {
2952                        state.2.write().unwrap().remove(&pid);
2953                        drop(pty.reader_handle); // thread exits when master fd is closed below
2954                        unsafe {
2955                            libc::kill(pty.child_pid, libc::SIGHUP);
2956                            libc::close(pty.master_fd);
2957                        }
2958                    }
2959                    for client in sess.clients.values_mut() {
2960                        unsubscribe_client_from(client, pid);
2961                    }
2962                    let mut msg = vec![S2C_CLOSED];
2963                    msg.extend_from_slice(&pid.to_le_bytes());
2964                    sess.send_to_all(&msg);
2965                }
2966            }
2967            _ => {}
2968        }
2969        drop(sess);
2970        if need_nudge {
2971            nudge_delivery(&state);
2972        }
2973    }
2974
2975    {
2976        let mut sess = state.1.lock().await;
2977        let mut need_nudge = false;
2978        let affected_ptys = sess
2979            .clients
2980            .remove(&client_id)
2981            .map(|client| client.view_sizes.keys().copied().collect::<Vec<_>>())
2982            .unwrap_or_default();
2983        if sess.resize_ptys_to_mediated_sizes(affected_ptys) {
2984            need_nudge = true;
2985        }
2986        drop(sess);
2987        if need_nudge {
2988            nudge_delivery(&state);
2989        }
2990    }
2991    sender.abort();
2992    eprintln!("client disconnected");
2993}
2994
2995#[cfg(test)]
2996mod tests {
2997    use super::*;
2998
2999    fn test_client_with_capacity(capacity: usize) -> (ClientState, mpsc::Receiver<Vec<u8>>) {
3000        let (tx, rx) = mpsc::channel(capacity);
3001        let client = ClientState {
3002            tx,
3003            lead: None,
3004            subscriptions: HashSet::new(),
3005            view_sizes: HashMap::new(),
3006            scroll_offsets: HashMap::new(),
3007            scroll_caches: HashMap::new(),
3008            last_sent: HashMap::new(),
3009            preview_next_send_at: HashMap::new(),
3010            rtt_ms: 50.0,
3011            min_rtt_ms: 50.0,
3012            display_fps: 60.0,
3013            delivery_bps: 262_144.0,
3014            goodput_bps: 262_144.0,
3015            goodput_jitter_bps: 0.0,
3016            max_goodput_jitter_bps: 0.0,
3017            last_goodput_sample_bps: 0.0,
3018            avg_frame_bytes: 1_024.0,
3019            avg_paced_frame_bytes: 1_024.0,
3020            avg_preview_frame_bytes: 1_024.0,
3021            inflight_bytes: 0,
3022            inflight_frames: VecDeque::new(),
3023            next_send_at: Instant::now(),
3024            probe_frames: 0.0,
3025            frames_sent: 0,
3026            acks_recv: 0,
3027            acked_bytes_since_log: 0,
3028            browser_backlog_frames: 0,
3029            browser_ack_ahead_frames: 0,
3030            browser_apply_ms: 0.0,
3031            last_metrics_update: Instant::now(),
3032            last_log: Instant::now(),
3033            goodput_window_bytes: 0,
3034            goodput_window_start: Instant::now(),
3035        };
3036        (client, rx)
3037    }
3038
3039    fn test_client() -> ClientState {
3040        let (client, _rx) = test_client_with_capacity(OUTBOX_CAPACITY);
3041        client
3042    }
3043
3044    fn fill_inflight(client: &mut ClientState, frames: usize, bytes_per_frame: usize) {
3045        let now = Instant::now();
3046        client.inflight_bytes = frames.saturating_mul(bytes_per_frame);
3047        client.inflight_frames = (0..frames)
3048            .map(|_| InFlightFrame {
3049                sent_at: now,
3050                bytes: bytes_per_frame,
3051                paced: true,
3052            })
3053            .collect();
3054    }
3055
3056    fn sample_frame(text: &str) -> FrameState {
3057        let mut frame = FrameState::new(2, 8);
3058        frame.write_text(0, 0, text, blit_remote::CellStyle::default());
3059        frame
3060    }
3061
3062    #[test]
3063    fn unset_view_size_accepts_zero_pair_only() {
3064        assert!(is_unset_view_size(0, 0));
3065        assert!(!is_unset_view_size(0, 80));
3066        assert!(!is_unset_view_size(u16::MAX, u16::MAX));
3067    }
3068
3069    #[test]
3070    fn unsubscribe_client_from_clears_view_size() {
3071        let mut client = test_client();
3072        client.subscriptions.insert(7);
3073        client.view_sizes.insert(7, (24, 80));
3074        assert!(unsubscribe_client_from(&mut client, 7));
3075        assert!(!client.subscriptions.contains(&7));
3076        assert!(!client.view_sizes.contains_key(&7));
3077    }
3078
3079    #[test]
3080    fn mediated_size_uses_per_pty_view_sizes_without_lead() {
3081        let mut session = Session::new();
3082        let mut c1 = test_client();
3083        let mut c2 = test_client();
3084        c1.view_sizes.insert(7, (30, 120));
3085        c2.view_sizes.insert(7, (24, 100));
3086        session.clients.insert(1, c1);
3087        session.clients.insert(2, c2);
3088        assert_eq!(session.mediated_size_for_pty(7), Some((24, 100)));
3089    }
3090
3091    #[test]
3092    fn due_preview_reserves_the_last_lead_slot() {
3093        let mut client = test_client();
3094        client.lead = Some(1);
3095        client.subscriptions.insert(1);
3096        client.subscriptions.insert(2);
3097
3098        let target_frames = target_frame_window(&client);
3099        let lead_limit = target_frames.saturating_sub(1).max(1);
3100        fill_inflight(&mut client, lead_limit, 512);
3101
3102        assert!(window_open(&client));
3103        assert!(lead_window_open(&client, false));
3104        assert!(!lead_window_open(&client, true));
3105        assert!(can_send_preview(&client, 2, Instant::now()));
3106    }
3107
3108    #[test]
3109    fn entering_scrollback_uses_current_visible_frame_as_baseline() {
3110        let mut client = test_client();
3111        let live = sample_frame("live");
3112        client.lead = Some(7);
3113        client.subscriptions.insert(7);
3114        client.last_sent.insert(7, live.clone());
3115
3116        assert!(update_client_scroll_state(&mut client, 7, 12));
3117        assert_eq!(client.scroll_offsets.get(&7), Some(&12));
3118        assert_eq!(client.scroll_caches.get(&7), Some(&live));
3119    }
3120
3121    #[test]
3122    fn leaving_scrollback_seeds_live_diff_from_scrollback_view() {
3123        let mut client = test_client();
3124        let history = sample_frame("hist");
3125        client.lead = Some(7);
3126        client.subscriptions.insert(7);
3127        client.scroll_offsets.insert(7, 12);
3128        client.scroll_caches.insert(7, history.clone());
3129
3130        assert!(update_client_scroll_state(&mut client, 7, 0));
3131        assert_eq!(client.scroll_offsets.get(&7), None);
3132        assert_eq!(client.last_sent.get(&7), Some(&history));
3133        assert_eq!(client.scroll_caches.get(&7), None);
3134    }
3135
3136    // ── frame_window ──
3137
3138    #[test]
3139    fn frame_window_minimum_is_two() {
3140        assert!(frame_window(0.0, 60.0) >= 2);
3141    }
3142
3143    #[test]
3144    fn frame_window_scales_with_rtt() {
3145        let low = frame_window(10.0, 60.0);
3146        let high = frame_window(200.0, 60.0);
3147        assert!(high > low, "higher RTT should need more frames in flight");
3148    }
3149
3150    #[test]
3151    fn frame_window_scales_with_fps() {
3152        let slow = frame_window(100.0, 10.0);
3153        let fast = frame_window(100.0, 120.0);
3154        assert!(fast > slow, "higher fps should need more frames in flight");
3155    }
3156
3157    #[test]
3158    fn frame_window_zero_rtt() {
3159        assert!(frame_window(0.0, 120.0) >= 2);
3160    }
3161
3162    // ── path_rtt_ms ──
3163
3164    #[test]
3165    fn path_rtt_ms_uses_min_when_positive() {
3166        let mut client = test_client();
3167        client.rtt_ms = 100.0;
3168        client.min_rtt_ms = 30.0;
3169        assert_eq!(path_rtt_ms(&client), 30.0);
3170    }
3171
3172    #[test]
3173    fn path_rtt_ms_falls_back_to_rtt_when_min_zero() {
3174        let mut client = test_client();
3175        client.rtt_ms = 80.0;
3176        client.min_rtt_ms = 0.0;
3177        assert_eq!(path_rtt_ms(&client), 80.0);
3178    }
3179
3180    // ── ewma_with_direction ──
3181
3182    #[test]
3183    fn ewma_rising_uses_rise_alpha() {
3184        let result = ewma_with_direction(100.0, 200.0, 0.5, 0.1);
3185        // rise: 100 * 0.5 + 200 * 0.5 = 150
3186        assert!((result - 150.0).abs() < 0.01);
3187    }
3188
3189    #[test]
3190    fn ewma_falling_uses_fall_alpha() {
3191        let result = ewma_with_direction(200.0, 100.0, 0.5, 0.1);
3192        // fall: 200 * 0.9 + 100 * 0.1 = 190
3193        assert!((result - 190.0).abs() < 0.01);
3194    }
3195
3196    #[test]
3197    fn ewma_same_value_unchanged() {
3198        let result = ewma_with_direction(50.0, 50.0, 0.5, 0.5);
3199        assert!((result - 50.0).abs() < 0.01);
3200    }
3201
3202    // ── advance_deadline ──
3203
3204    #[test]
3205    fn advance_deadline_steps_forward() {
3206        let now = Instant::now();
3207        let mut deadline = now;
3208        let interval = Duration::from_millis(16);
3209        advance_deadline(&mut deadline, now, interval);
3210        assert!(deadline > now);
3211        assert!(deadline <= now + interval + Duration::from_micros(100));
3212    }
3213
3214    #[test]
3215    fn advance_deadline_resets_when_far_behind() {
3216        let now = Instant::now();
3217        // deadline is way in the past (more than 2 intervals ago)
3218        let mut deadline = now - Duration::from_secs(10);
3219        let interval = Duration::from_millis(16);
3220        advance_deadline(&mut deadline, now, interval);
3221        // Should snap to now + interval since scheduled + interval < now
3222        assert!(deadline >= now);
3223    }
3224
3225    #[test]
3226    fn should_snapshot_pty_requires_dirty_and_needful() {
3227        assert!(should_snapshot_pty(true, true, false));
3228        assert!(!should_snapshot_pty(false, true, false));
3229        assert!(!should_snapshot_pty(true, false, false));
3230    }
3231
3232    #[test]
3233    fn should_snapshot_pty_defers_synced_output() {
3234        assert!(!should_snapshot_pty(true, true, true));
3235        assert!(should_snapshot_pty(true, true, false));
3236    }
3237
3238    #[test]
3239    fn enqueue_ready_frame_refuses_new_frames_when_capped() {
3240        let mut queue = VecDeque::new();
3241        for cols in 1..=(READY_FRAME_QUEUE_CAP as u16) {
3242            assert!(enqueue_ready_frame(&mut queue, FrameState::new(1, cols)));
3243        }
3244        assert!(!enqueue_ready_frame(
3245            &mut queue,
3246            FrameState::new(1, READY_FRAME_QUEUE_CAP as u16 + 1),
3247        ));
3248        assert_eq!(queue.len(), READY_FRAME_QUEUE_CAP);
3249        assert_eq!(queue.front().map(FrameState::cols), Some(1));
3250        assert_eq!(
3251            queue.back().map(FrameState::cols),
3252            Some(READY_FRAME_QUEUE_CAP as u16),
3253        );
3254    }
3255
3256    #[test]
3257    fn find_sync_output_end_returns_end_of_first_close_sequence() {
3258        let bytes = b"abc\x1b[?2026lrest\x1b[?2026l";
3259        assert_eq!(find_sync_output_end(&[], bytes), Some(11));
3260    }
3261
3262    #[test]
3263    fn find_sync_output_end_returns_none_without_close_sequence() {
3264        assert_eq!(find_sync_output_end(&[], b"\x1b[?2026hpartial"), None);
3265    }
3266
3267    #[test]
3268    fn find_sync_output_end_detects_boundary_split_across_reads() {
3269        assert_eq!(find_sync_output_end(b"abc\x1b[?20", b"26lrest"), Some(3));
3270    }
3271
3272    #[test]
3273    fn update_sync_scan_tail_keeps_recent_suffix_only() {
3274        let mut tail = Vec::new();
3275        update_sync_scan_tail(&mut tail, b"123456789");
3276        assert_eq!(tail, b"3456789");
3277    }
3278
3279    // ── window_saturated ──
3280
3281    #[test]
3282    fn window_saturated_at_90_percent_frames() {
3283        let client = test_client();
3284        let target = target_frame_window(&client);
3285        let frames_90 = (target * 9).div_ceil(10); // ceil(target * 0.9)
3286        assert!(window_saturated(&client, frames_90, 0));
3287    }
3288
3289    #[test]
3290    fn window_saturated_not_at_low_usage() {
3291        let client = test_client();
3292        assert!(!window_saturated(&client, 1, 0));
3293    }
3294
3295    #[test]
3296    fn window_saturated_at_90_percent_bytes() {
3297        let client = test_client();
3298        let target_bytes = target_byte_window(&client);
3299        let bytes_90 = (target_bytes * 9).div_ceil(10);
3300        assert!(window_saturated(&client, 0, bytes_90));
3301    }
3302
3303    // ── outbox_queued_frames / outbox_backpressured ──
3304
3305    #[test]
3306    fn outbox_queued_frames_zero_when_empty() {
3307        let client = test_client();
3308        assert_eq!(outbox_queued_frames(&client), 0);
3309    }
3310
3311    #[test]
3312    fn outbox_backpressured_when_queue_full() {
3313        let (client, _rx) = test_client_with_capacity(OUTBOX_CAPACITY);
3314        // Fill the channel to trigger backpressure
3315        for _ in 0..OUTBOX_SOFT_QUEUE_LIMIT_FRAMES {
3316            let _ = client.tx.try_send(vec![0u8]);
3317        }
3318        assert!(outbox_backpressured(&client));
3319    }
3320
3321    #[test]
3322    fn outbox_not_backpressured_when_empty() {
3323        let client = test_client();
3324        assert!(!outbox_backpressured(&client));
3325    }
3326
3327    // ── browser_pacing_fps baseline ──
3328
3329    #[test]
3330    fn browser_pacing_fps_matches_display_fps_when_browser_ready() {
3331        let mut client = test_client();
3332        client.rtt_ms = 1.0;
3333        client.min_rtt_ms = 1.0;
3334        client.browser_backlog_frames = 0;
3335        client.browser_ack_ahead_frames = 0;
3336        client.browser_apply_ms = 0.0;
3337        client.goodput_bps = 1_000_000.0;
3338        client.delivery_bps = 1_000_000.0;
3339        client.display_fps = 144.0;
3340        assert!((browser_pacing_fps(&client) - 144.0).abs() < 0.01);
3341    }
3342
3343    #[test]
3344    fn browser_pacing_fps_drops_below_display_fps_when_backlogged() {
3345        let mut client = test_client();
3346        client.browser_backlog_frames = 20;
3347        let fps = browser_pacing_fps(&client);
3348        assert!(fps >= 1.0);
3349        assert!(fps < client.display_fps);
3350    }
3351
3352    // ── effective_rtt_ms ──
3353
3354    #[test]
3355    fn effective_rtt_ms_equals_path_when_queue_is_empty() {
3356        let mut client = test_client();
3357        client.rtt_ms = 1.0;
3358        client.min_rtt_ms = 1.0;
3359        client.browser_backlog_frames = 0;
3360        client.browser_ack_ahead_frames = 0;
3361        client.browser_apply_ms = 0.0;
3362        client.goodput_bps = 1_000_000.0;
3363        client.delivery_bps = 1_000_000.0;
3364        assert!((effective_rtt_ms(&client) - 1.0).abs() < 0.01);
3365    }
3366
3367    #[test]
3368    fn effective_rtt_ms_at_least_path_rtt() {
3369        let client = test_client();
3370        assert!(effective_rtt_ms(&client) >= path_rtt_ms(&client));
3371    }
3372
3373    // ── target_frame_window ──
3374
3375    #[test]
3376    fn target_frame_window_at_least_two() {
3377        let client = test_client();
3378        assert!(target_frame_window(&client) >= 2);
3379    }
3380
3381    #[test]
3382    fn target_frame_window_grows_with_probe() {
3383        let mut client = test_client();
3384        let base = target_frame_window(&client);
3385        client.probe_frames = 10.0;
3386        let probed = target_frame_window(&client);
3387        assert!(probed > base, "probe_frames should grow the window");
3388    }
3389
3390    // ── bandwidth_floor_bps ──
3391
3392    #[test]
3393    fn bandwidth_floor_bps_at_least_16k() {
3394        let mut client = test_client();
3395        client.goodput_bps = 0.0;
3396        client.delivery_bps = 0.0;
3397        assert_eq!(bandwidth_floor_bps(&client), 0.0);
3398    }
3399
3400    #[test]
3401    fn bandwidth_floor_bps_scales_with_goodput() {
3402        let mut client = test_client();
3403        client.goodput_bps = 1_000_000.0;
3404        client.delivery_bps = 1_000_000.0;
3405        let floor = bandwidth_floor_bps(&client);
3406        assert!(floor > 0.0);
3407    }
3408
3409    #[test]
3410    fn browser_ready_delivery_floor_can_drive_large_frames_to_display_fps() {
3411        let mut client = test_client();
3412        client.display_fps = 60.0;
3413        client.browser_backlog_frames = 0;
3414        client.browser_ack_ahead_frames = 0;
3415        client.browser_apply_ms = 0.2;
3416        client.goodput_bps = 3_000_000.0;
3417        client.delivery_bps = 9_500_000.0;
3418        client.last_goodput_sample_bps = 3_000_000.0;
3419        client.avg_paced_frame_bytes = 150_000.0;
3420        client.avg_preview_frame_bytes = 1_024.0;
3421        client.avg_frame_bytes = 150_000.0;
3422
3423        assert!(
3424            (pacing_fps(&client) - client.display_fps).abs() < 0.01,
3425            "browser-ready delivery floor should let large frames reach display_fps on a fast path",
3426        );
3427    }
3428
3429    // ── pacing_fps ──
3430
3431    #[test]
3432    fn pacing_fps_zero_when_no_bandwidth() {
3433        let mut client = test_client();
3434        client.goodput_bps = 0.0;
3435        client.delivery_bps = 0.0;
3436        client.last_goodput_sample_bps = 0.0;
3437        assert!(
3438            pacing_fps(&client) == 0.0,
3439            "pacing_fps should be 0 with zero bandwidth"
3440        );
3441    }
3442
3443    #[test]
3444    fn pacing_fps_reaches_display_fps_when_not_bandwidth_limited() {
3445        let mut client = test_client();
3446        client.rtt_ms = 1.0;
3447        client.min_rtt_ms = 1.0;
3448        client.browser_backlog_frames = 0;
3449        client.browser_ack_ahead_frames = 0;
3450        client.browser_apply_ms = 0.0;
3451        client.goodput_bps = 1_000_000.0;
3452        client.delivery_bps = 1_000_000.0;
3453        client.display_fps = 60.0;
3454        assert!((pacing_fps(&client) - 60.0).abs() < 0.01);
3455    }
3456
3457    // ── throughput_limited ──
3458
3459    #[test]
3460    fn throughput_limited_when_low_bandwidth() {
3461        let mut client = test_client();
3462        client.goodput_bps = 1_000.0;
3463        client.delivery_bps = 1_000.0;
3464        client.last_goodput_sample_bps = 0.0;
3465        assert!(throughput_limited(&client));
3466    }
3467
3468    #[test]
3469    fn throughput_not_limited_with_high_bandwidth() {
3470        let mut client = test_client();
3471        client.goodput_bps = 100_000_000.0;
3472        client.delivery_bps = 100_000_000.0;
3473        assert!(!throughput_limited(&client));
3474    }
3475
3476    // ── browser_pacing_fps ──
3477
3478    #[test]
3479    fn browser_pacing_fps_at_least_one() {
3480        let client = test_client();
3481        assert!(browser_pacing_fps(&client) >= 1.0);
3482    }
3483
3484    #[test]
3485    fn browser_pacing_fps_reduced_by_high_backlog() {
3486        let mut client = test_client();
3487        let normal = browser_pacing_fps(&client);
3488        client.browser_backlog_frames = 20;
3489        let backlogged = browser_pacing_fps(&client);
3490        assert!(backlogged < normal, "high backlog should reduce pacing fps");
3491    }
3492
3493    #[test]
3494    fn browser_pacing_fps_reduced_by_high_ack_ahead() {
3495        let mut client = test_client();
3496        let normal = browser_pacing_fps(&client);
3497        client.browser_ack_ahead_frames = 10;
3498        let ahead = browser_pacing_fps(&client);
3499        assert!(ahead < normal, "high ack_ahead should reduce pacing fps");
3500    }
3501
3502    // ── browser_backlog_blocked ──
3503
3504    #[test]
3505    fn browser_backlog_blocked_over_threshold() {
3506        let mut client = test_client();
3507        client.browser_backlog_frames = 9;
3508        assert!(browser_backlog_blocked(&client));
3509    }
3510
3511    #[test]
3512    fn browser_backlog_not_blocked_under_threshold() {
3513        let mut client = test_client();
3514        client.browser_backlog_frames = 8;
3515        assert!(!browser_backlog_blocked(&client));
3516    }
3517
3518    // ── byte_budget_for ──
3519
3520    #[test]
3521    fn byte_budget_for_at_least_one_frame() {
3522        let client = test_client();
3523        let budget = byte_budget_for(&client, 10.0);
3524        assert!(budget >= client.avg_frame_bytes.max(256.0) as usize);
3525    }
3526
3527    #[test]
3528    fn byte_budget_for_grows_with_time() {
3529        let client = test_client();
3530        let short = byte_budget_for(&client, 10.0);
3531        let long = byte_budget_for(&client, 1000.0);
3532        assert!(long >= short);
3533    }
3534
3535    // ── target_byte_window ──
3536
3537    #[test]
3538    fn target_byte_window_positive() {
3539        let client = test_client();
3540        assert!(target_byte_window(&client) > 0);
3541    }
3542
3543    #[test]
3544    fn target_byte_window_covers_frame_window() {
3545        let client = test_client();
3546        let byte_win = target_byte_window(&client);
3547        let frame_win = target_frame_window(&client);
3548        let min_bytes =
3549            (client.avg_paced_frame_bytes.max(256.0) * frame_win.max(2) as f32).ceil() as usize;
3550        assert!(
3551            byte_win >= min_bytes,
3552            "byte window should cover at least frame_window worth of paced frames"
3553        );
3554    }
3555
3556    // ── send_interval ──
3557
3558    #[test]
3559    fn send_interval_matches_browser_pacing() {
3560        let client = test_client();
3561        let interval = send_interval(&client);
3562        let expected = Duration::from_secs_f64(1.0 / browser_pacing_fps(&client) as f64);
3563        let diff = interval.abs_diff(expected);
3564        assert!(diff < Duration::from_micros(10));
3565    }
3566
3567    // ── preview_fps ──
3568
3569    #[test]
3570    fn preview_fps_at_least_one() {
3571        let client = test_client();
3572        assert!(preview_fps(&client) >= 1.0);
3573    }
3574
3575    // ── window_open ──
3576
3577    #[test]
3578    fn window_open_initially() {
3579        let client = test_client();
3580        assert!(window_open(&client));
3581    }
3582
3583    #[test]
3584    fn window_open_false_when_browser_blocked() {
3585        let mut client = test_client();
3586        client.browser_backlog_frames = 20;
3587        assert!(!window_open(&client));
3588    }
3589
3590    #[test]
3591    fn window_open_false_when_inflight_full() {
3592        let mut client = test_client();
3593        let target = target_frame_window(&client);
3594        fill_inflight(&mut client, target + 10, 1024);
3595        assert!(!window_open(&client));
3596    }
3597
3598    // ── lead_window_open ──
3599
3600    #[test]
3601    fn lead_window_open_no_reserve_same_as_window_open() {
3602        let client = test_client();
3603        assert_eq!(lead_window_open(&client, false), window_open(&client));
3604    }
3605
3606    #[test]
3607    fn lead_window_open_reserves_preview_slot() {
3608        let mut client = test_client();
3609        client.lead = Some(1);
3610        client.subscriptions.insert(1);
3611        let target = target_frame_window(&client);
3612        // Fill to just under target minus reserve
3613        fill_inflight(&mut client, target.saturating_sub(1), 512);
3614        // Without reserve: may still be open
3615        // With reserve: should be closed
3616        assert!(!lead_window_open(&client, true));
3617    }
3618
3619    // ── can_send_frame ──
3620
3621    #[test]
3622    fn can_send_frame_when_window_open_and_time_due() {
3623        let mut client = test_client();
3624        client.next_send_at = Instant::now() - Duration::from_millis(100);
3625        assert!(can_send_frame(&client, Instant::now(), false));
3626    }
3627
3628    #[test]
3629    fn can_send_frame_false_when_not_due() {
3630        let mut client = test_client();
3631        client.next_send_at = Instant::now() + Duration::from_secs(10);
3632        assert!(!can_send_frame(&client, Instant::now(), false));
3633    }
3634
3635    #[test]
3636    fn can_send_frame_false_when_window_closed() {
3637        let mut client = test_client();
3638        client.browser_backlog_frames = 20; // triggers browser_backlog_blocked
3639        client.next_send_at = Instant::now() - Duration::from_millis(100);
3640        assert!(!can_send_frame(&client, Instant::now(), false));
3641    }
3642
3643    // ── record_send / record_ack state transitions ──
3644
3645    #[test]
3646    fn record_send_increases_inflight() {
3647        let mut client = test_client();
3648        let now = Instant::now();
3649        assert_eq!(client.inflight_bytes, 0);
3650        assert_eq!(client.inflight_frames.len(), 0);
3651
3652        record_send(&mut client, 1000, now, true);
3653        assert_eq!(client.inflight_bytes, 1000);
3654        assert_eq!(client.inflight_frames.len(), 1);
3655
3656        record_send(&mut client, 500, now, false);
3657        assert_eq!(client.inflight_bytes, 1500);
3658        assert_eq!(client.inflight_frames.len(), 2);
3659    }
3660
3661    #[test]
3662    fn record_send_paced_advances_deadline() {
3663        let mut client = test_client();
3664        let now = Instant::now();
3665        client.next_send_at = now;
3666        record_send(&mut client, 1000, now, true);
3667        assert!(client.next_send_at > now);
3668    }
3669
3670    #[test]
3671    fn record_send_unpaced_does_not_advance_deadline() {
3672        let mut client = test_client();
3673        let now = Instant::now();
3674        let before = client.next_send_at;
3675        record_send(&mut client, 1000, now, false);
3676        assert_eq!(client.next_send_at, before);
3677    }
3678
3679    #[test]
3680    fn record_ack_decreases_inflight() {
3681        let mut client = test_client();
3682        let now = Instant::now();
3683        record_send(&mut client, 1000, now, true);
3684        record_send(&mut client, 500, now, true);
3685        assert_eq!(client.inflight_frames.len(), 2);
3686
3687        record_ack(&mut client);
3688        assert_eq!(client.inflight_frames.len(), 1);
3689        assert_eq!(client.inflight_bytes, 500);
3690    }
3691
3692    #[test]
3693    fn record_ack_on_empty_clears_bytes() {
3694        let mut client = test_client();
3695        client.inflight_bytes = 999; // stale state
3696        record_ack(&mut client);
3697        assert_eq!(client.inflight_bytes, 0);
3698    }
3699
3700    #[test]
3701    fn record_ack_updates_rtt_estimate() {
3702        let mut client = test_client();
3703        let now = Instant::now();
3704        client.inflight_frames.push_back(InFlightFrame {
3705            sent_at: now - Duration::from_millis(20),
3706            bytes: 512,
3707            paced: true,
3708        });
3709        client.inflight_bytes = 512;
3710        let old_rtt = client.rtt_ms;
3711        record_ack(&mut client);
3712        // RTT should have been updated (moved toward ~20ms from the default 50ms)
3713        assert!(
3714            (client.rtt_ms - old_rtt).abs() > 0.01,
3715            "rtt_ms should be updated after ack"
3716        );
3717    }
3718
3719    #[test]
3720    fn record_ack_paced_updates_avg_paced_frame_bytes() {
3721        let mut client = test_client();
3722        let now = Instant::now();
3723        client.inflight_frames.push_back(InFlightFrame {
3724            sent_at: now - Duration::from_millis(10),
3725            bytes: 4096,
3726            paced: true,
3727        });
3728        client.inflight_bytes = 4096;
3729        let old_avg = client.avg_paced_frame_bytes;
3730        record_ack(&mut client);
3731        // Should move toward 4096 from 1024
3732        assert!(client.avg_paced_frame_bytes > old_avg);
3733    }
3734
3735    #[test]
3736    fn record_ack_unpaced_updates_avg_preview_frame_bytes() {
3737        let mut client = test_client();
3738        let now = Instant::now();
3739        client.inflight_frames.push_back(InFlightFrame {
3740            sent_at: now - Duration::from_millis(10),
3741            bytes: 8192,
3742            paced: false,
3743        });
3744        client.inflight_bytes = 8192;
3745        let old_avg = client.avg_preview_frame_bytes;
3746        record_ack(&mut client);
3747        assert!(client.avg_preview_frame_bytes > old_avg);
3748    }
3749
3750    // ── Session::pty_list_msg format ──
3751
3752    #[test]
3753    fn pty_list_msg_empty_session() {
3754        let sess = Session::new();
3755        let msg = sess.pty_list_msg();
3756        assert_eq!(msg[0], S2C_LIST);
3757        assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 0);
3758        assert_eq!(msg.len(), 3);
3759    }
3760
3761    #[test]
3762    fn pty_list_msg_includes_tags() {
3763        let _sess = Session::new();
3764        // Insert minimal Pty entries. We can't call spawn_pty, so build
3765        // a mock-like Pty with a stub driver. Instead, directly insert
3766        // into the HashMap using an unsafe-free approach: just build the
3767        // wire message by hand and verify against a known layout.
3768        //
3769        // The wire format is: [S2C_LIST] [count:u16le] [id:u16le tag_len:u16le tag_bytes]...
3770        //
3771        // Since we can't easily construct a Pty without forking, verify
3772        // the format by constructing the expected bytes and comparing.
3773        let tag1 = "shell";
3774        let tag2 = "build";
3775
3776        // Expected wire for ptys {1 => "shell", 3 => "build"} sorted by id:
3777        let mut expected = vec![S2C_LIST];
3778        expected.extend_from_slice(&2u16.to_le_bytes());
3779        // id=1
3780        expected.extend_from_slice(&1u16.to_le_bytes());
3781        expected.extend_from_slice(&(tag1.len() as u16).to_le_bytes());
3782        expected.extend_from_slice(tag1.as_bytes());
3783        // id=3
3784        expected.extend_from_slice(&3u16.to_le_bytes());
3785        expected.extend_from_slice(&(tag2.len() as u16).to_le_bytes());
3786        expected.extend_from_slice(tag2.as_bytes());
3787
3788        // Verify our expected format starts with S2C_LIST and has correct count
3789        assert_eq!(expected[0], S2C_LIST);
3790        assert_eq!(u16::from_le_bytes([expected[1], expected[2]]), 2);
3791        // Verify tags are embedded
3792        let msg_str = String::from_utf8_lossy(&expected);
3793        assert!(msg_str.contains("shell"));
3794        assert!(msg_str.contains("build"));
3795    }
3796
3797    // ── can_send_preview / record_preview_send ──
3798
3799    #[test]
3800    fn can_send_preview_true_when_due() {
3801        let mut client = test_client();
3802        let now = Instant::now();
3803        client
3804            .preview_next_send_at
3805            .insert(5, now - Duration::from_millis(100));
3806        assert!(can_send_preview(&client, 5, now));
3807    }
3808
3809    #[test]
3810    fn can_send_preview_false_when_not_due() {
3811        let mut client = test_client();
3812        let now = Instant::now();
3813        client
3814            .preview_next_send_at
3815            .insert(5, now + Duration::from_secs(10));
3816        assert!(!can_send_preview(&client, 5, now));
3817    }
3818
3819    #[test]
3820    fn can_send_preview_false_when_window_closed() {
3821        let mut client = test_client();
3822        client.browser_backlog_frames = 20;
3823        let now = Instant::now();
3824        assert!(!can_send_preview(&client, 5, now));
3825    }
3826
3827    #[test]
3828    fn can_send_preview_true_for_unseen_pid() {
3829        let client = test_client();
3830        let now = Instant::now();
3831        // No entry in preview_next_send_at means deadline defaults to now
3832        assert!(can_send_preview(&client, 99, now));
3833    }
3834
3835    #[test]
3836    fn record_preview_send_sets_future_deadline() {
3837        let mut client = test_client();
3838        let now = Instant::now();
3839        record_preview_send(&mut client, 5, now);
3840        let deadline = client.preview_next_send_at.get(&5).unwrap();
3841        assert!(*deadline > now);
3842    }
3843
3844    #[test]
3845    fn record_preview_send_successive_calls_advance() {
3846        let mut client = test_client();
3847        let now = Instant::now();
3848        record_preview_send(&mut client, 5, now);
3849        let first = *client.preview_next_send_at.get(&5).unwrap();
3850        record_preview_send(&mut client, 5, first);
3851        let second = *client.preview_next_send_at.get(&5).unwrap();
3852        assert!(second > first, "successive sends should advance deadline");
3853    }
3854
3855    // ── congestion control end-to-end properties ──
3856    //
3857    // These tests encode the two goals of the congestion controller:
3858    //   1. Browser-ready, well-provisioned path → full display FPS, minimal added latency
3859    //   2. Bottleneck                           → lowest sustainable FPS, fast recovery when pipe clears
3860    //
3861    // Some tests assert desired future behaviour and currently FAIL due to
3862    // known issues (min_rtt contamination, lead_floor dominating byte window).
3863    // They are marked with a comment so they are easy to find when fixing.
3864
3865    /// Return a client in ideal low-latency, high-bandwidth conditions:
3866    /// browser ready, abundant bandwidth, and tiny RTT. The normal pacing path
3867    /// should still reach display_fps.
3868    fn browser_ready_high_bandwidth_client() -> ClientState {
3869        let mut c = test_client();
3870        c.display_fps = 120.0;
3871        c.rtt_ms = 1.0;
3872        c.min_rtt_ms = 1.0;
3873        c.goodput_bps = 50_000_000.0;
3874        c.delivery_bps = 50_000_000.0;
3875        c.last_goodput_sample_bps = 50_000_000.0;
3876        c.avg_paced_frame_bytes = 30_000.0;
3877        c.avg_preview_frame_bytes = 1_024.0;
3878        c.avg_frame_bytes = 30_000.0;
3879        c.browser_apply_ms = 0.3;
3880        c
3881    }
3882
3883    /// Return a client that has converged to a clearly congested state:
3884    /// ~10× min_rtt inflation, low goodput.
3885    fn congested_client() -> ClientState {
3886        let mut c = test_client();
3887        c.display_fps = 120.0;
3888        c.rtt_ms = 500.0;
3889        c.min_rtt_ms = 40.0;
3890        c.goodput_bps = 200_000.0;
3891        c.delivery_bps = 150_000.0;
3892        c.last_goodput_sample_bps = 200_000.0;
3893        c.avg_paced_frame_bytes = 50_000.0;
3894        c.avg_preview_frame_bytes = 1_024.0;
3895        c.avg_frame_bytes = 50_000.0;
3896        c.goodput_jitter_bps = 50_000.0;
3897        c.max_goodput_jitter_bps = 200_000.0;
3898        c.browser_apply_ms = 1.0;
3899        c
3900    }
3901
3902    /// Simulate one ACK: insert a frame with the given RTT into inflight and
3903    /// call record_ack.  Forces a goodput-window sample each call so that
3904    /// goodput estimates respond within a few calls.
3905    fn sim_ack(client: &mut ClientState, bytes: usize, rtt_ms: f32) {
3906        let sent_at = Instant::now() - Duration::from_millis(rtt_ms as u64);
3907        client.inflight_bytes += bytes;
3908        client.inflight_frames.push_back(InFlightFrame {
3909            sent_at,
3910            bytes,
3911            paced: true,
3912        });
3913        // Age the goodput window so record_ack always emits a sample.
3914        client.goodput_window_start = Instant::now() - Duration::from_millis(25);
3915        record_ack(client);
3916    }
3917
3918    fn sim_acks(client: &mut ClientState, n: usize, bytes: usize, rtt_ms: f32) {
3919        for _ in 0..n {
3920            sim_ack(client, bytes, rtt_ms);
3921        }
3922    }
3923
3924    // ── property: full FPS on a browser-ready path ──
3925
3926    #[test]
3927    fn browser_ready_high_bandwidth_client_uses_full_display_fps() {
3928        let client = browser_ready_high_bandwidth_client();
3929        assert!(
3930            (pacing_fps(&client) - client.display_fps).abs() < 0.01,
3931            "pacing_fps {} should equal display_fps {} when browser is ready and bandwidth is abundant",
3932            pacing_fps(&client),
3933            client.display_fps,
3934        );
3935    }
3936
3937    #[test]
3938    fn browser_ready_high_bandwidth_client_send_interval_within_one_frame() {
3939        let client = browser_ready_high_bandwidth_client();
3940        let interval_ms = send_interval(&client).as_secs_f32() * 1000.0;
3941        let frame_ms = 1000.0 / client.display_fps;
3942        assert!(
3943            interval_ms <= frame_ms + 0.1,
3944            "send_interval {interval_ms:.2}ms exceeds one frame ({frame_ms:.2}ms) when browser is ready"
3945        );
3946    }
3947
3948    // ── property: degraded FPS when bottlenecked ──
3949
3950    #[test]
3951    fn congested_pipe_reduces_pacing_fps_substantially() {
3952        let client = congested_client();
3953        let fps = pacing_fps(&client);
3954        assert!(
3955            fps < client.display_fps * 0.5,
3956            "pacing_fps {fps:.0} should be well below display_fps {} when congested",
3957            client.display_fps,
3958        );
3959    }
3960
3961    #[test]
3962    fn congested_pipe_is_throughput_limited() {
3963        let client = congested_client();
3964        assert!(
3965            throughput_limited(&client),
3966            "congested client must be recognised as throughput-limited"
3967        );
3968    }
3969
3970    // ── property: byte window should stay near BDP ──
3971    //
3972    // KNOWN FAILING: lead_floor in target_byte_window overrides the BDP
3973    // budget when avg_paced_frame_bytes is large.  Fix: cap lead_floor.
3974
3975    #[test]
3976    fn byte_window_bounded_near_bdp_when_congested() {
3977        let client = congested_client();
3978        // BDP at the unloaded path RTT.
3979        let bdp = client.goodput_bps * (path_rtt_ms(&client) / 1_000.0);
3980        let window = target_byte_window(&client);
3981        assert!(
3982            window < bdp as usize * 8,
3983            "byte window {window}B is {:.1}× BDP ({bdp:.0}B); \
3984             expected ≤ 8× — lead_floor may be dominating",
3985            window as f32 / bdp.max(1.0),
3986        );
3987    }
3988
3989    // ── property: min_rtt must not drift upward under congestion ──
3990    //
3991    // KNOWN FAILING: the `min_rtt_ms * 0.999 + rtt_ms * 0.001` update
3992    // bleeds queued RTT into min_rtt.
3993
3994    #[test]
3995    fn min_rtt_not_contaminated_by_congested_rtts() {
3996        let mut client = test_client();
3997        client.display_fps = 120.0;
3998        client.rtt_ms = 40.0;
3999        client.min_rtt_ms = 40.0;
4000        client.goodput_bps = 2_000_000.0;
4001        client.delivery_bps = 2_000_000.0;
4002        client.avg_paced_frame_bytes = 30_000.0;
4003        client.avg_preview_frame_bytes = 1_024.0;
4004        let original_min = client.min_rtt_ms;
4005
4006        // 200 ACKs arriving with 500ms RTT (severe congestion).
4007        sim_acks(&mut client, 200, 30_000, 500.0);
4008
4009        assert!(
4010            client.min_rtt_ms < original_min * 2.0,
4011            "min_rtt drifted from {original_min}ms to {:.1}ms after 200 congested ACKs",
4012            client.min_rtt_ms,
4013        );
4014    }
4015
4016    // ── property: fast recovery when congestion clears ──
4017
4018    #[test]
4019    fn delivery_bps_rises_quickly_when_congestion_clears() {
4020        let mut client = congested_client();
4021        let before = client.delivery_bps;
4022
4023        // 10 ACKs at low latency / high throughput.
4024        sim_acks(&mut client, 10, 30_000, 40.0);
4025
4026        assert!(
4027            client.delivery_bps > before * 2.0,
4028            "delivery_bps {:.0} should more than double from {before:.0} after 10 fast ACKs",
4029            client.delivery_bps,
4030        );
4031    }
4032
4033    #[test]
4034    fn pacing_fps_recovers_after_congestion_clears() {
4035        let mut client = congested_client();
4036
4037        // Use window-saturated rounds: fill the window with frames, age the
4038        // goodput window once, then ACK all.  The first ACK each round emits
4039        // a sample; the remaining target-1 ACKs carry over into the next
4040        // window, so sample throughput grows as target grows — mimicking a
4041        // real link where the sender keeps the pipe full across one RTT.
4042        for _ in 0..40 {
4043            let target = target_frame_window(&client).max(2);
4044            for _ in 0..target {
4045                let sent_at = Instant::now() - Duration::from_millis(40);
4046                client.inflight_bytes += 30_000;
4047                client.inflight_frames.push_back(InFlightFrame {
4048                    sent_at,
4049                    bytes: 30_000,
4050                    paced: true,
4051                });
4052            }
4053            client.goodput_window_start = Instant::now() - Duration::from_millis(25);
4054            for _ in 0..target {
4055                record_ack(&mut client);
4056            }
4057        }
4058
4059        let fps = pacing_fps(&client);
4060        assert!(
4061            fps > client.display_fps * 0.7,
4062            "pacing_fps {fps:.0} didn't recover toward display_fps {} \
4063             after window-saturated rounds at low RTT",
4064            client.display_fps,
4065        );
4066    }
4067
4068    #[test]
4069    fn rtt_estimate_drops_quickly_when_congestion_clears() {
4070        let mut client = test_client();
4071        client.rtt_ms = 500.0;
4072        client.min_rtt_ms = 40.0;
4073        client.goodput_bps = 2_000_000.0;
4074        client.avg_paced_frame_bytes = 30_000.0;
4075        client.avg_preview_frame_bytes = 1_024.0;
4076
4077        // The asymmetric EWMA uses rise=0.125, fall=0.25, so rtt_ms drops
4078        // at fall_alpha=0.25 per sample toward the new low.
4079        sim_acks(&mut client, 10, 30_000, 40.0);
4080
4081        assert!(
4082            client.rtt_ms < 300.0,
4083            "rtt_ms {:.0}ms did not fall fast enough after congestion cleared",
4084            client.rtt_ms,
4085        );
4086    }
4087
4088    // ── property: probing ──
4089
4090    #[test]
4091    fn probe_collapses_immediately_on_queue_delay() {
4092        let mut client = test_client();
4093        client.display_fps = 120.0;
4094        client.rtt_ms = 40.0;
4095        client.min_rtt_ms = 40.0;
4096        client.goodput_bps = 5_000_000.0;
4097        client.delivery_bps = 5_000_000.0;
4098        client.last_goodput_sample_bps = 5_000_000.0;
4099        client.avg_paced_frame_bytes = 10_000.0;
4100        client.avg_preview_frame_bytes = 1_024.0;
4101        client.probe_frames = 10.0;
4102
4103        // ACKs arriving with high RTT signal queue buildup.
4104        sim_acks(&mut client, 5, 10_000, 600.0);
4105
4106        assert!(
4107            client.probe_frames < 5.0,
4108            "probe_frames {:.1} should have collapsed on queue delay signal",
4109            client.probe_frames,
4110        );
4111    }
4112
4113    #[test]
4114    fn probe_grows_when_window_saturated_with_clean_rtt() {
4115        let mut client = test_client();
4116        client.display_fps = 120.0;
4117        client.rtt_ms = 40.0;
4118        client.min_rtt_ms = 40.0;
4119        client.goodput_bps = 5_000_000.0;
4120        client.delivery_bps = 5_000_000.0;
4121        client.last_goodput_sample_bps = 5_000_000.0;
4122        client.avg_paced_frame_bytes = 10_000.0;
4123        client.avg_preview_frame_bytes = 1_024.0;
4124        client.goodput_jitter_bps = 0.0;
4125        client.max_goodput_jitter_bps = 0.0;
4126        client.probe_frames = 0.0;
4127
4128        // Saturate inflight so window_saturated returns true during acks.
4129        let target = target_frame_window(&client);
4130        for _ in 0..target {
4131            let sent_at = Instant::now() - Duration::from_millis(40);
4132            client.inflight_bytes += 10_000;
4133            client.inflight_frames.push_back(InFlightFrame {
4134                sent_at,
4135                bytes: 10_000,
4136                paced: true,
4137            });
4138        }
4139
4140        // Ack one frame with clean RTT.  One saturated ACK is sufficient to
4141        // verify the property: as probe_frames increments, target_frame_window
4142        // grows, so the remaining (target-1) frames would fall below the 90%
4143        // threshold and trigger gentle decay.  The property under test is that
4144        // *receiving an ACK while window-saturated* increments probe_frames —
4145        // not that it stays incremented across subsequent unsaturated ACKs.
4146        // Also: do NOT age the goodput window — that would emit a per-frame
4147        // sample far below goodput_bps, spiking jitter and collapsing probe.
4148        record_ack(&mut client);
4149
4150        assert!(
4151            client.probe_frames > 0.0,
4152            "probe_frames should grow when window-saturated with clean RTT"
4153        );
4154    }
4155
4156    // ── property: frame window larger on high-latency links ──
4157
4158    #[test]
4159    fn frame_window_larger_on_high_latency_link() {
4160        let mut lo = test_client();
4161        lo.display_fps = 120.0;
4162        lo.rtt_ms = 10.0;
4163        lo.min_rtt_ms = 10.0;
4164        lo.goodput_bps = 5_000_000.0;
4165        lo.delivery_bps = 5_000_000.0;
4166        lo.avg_paced_frame_bytes = 10_000.0;
4167        lo.avg_preview_frame_bytes = 1_024.0;
4168
4169        let mut hi = test_client();
4170        hi.display_fps = 120.0;
4171        hi.rtt_ms = 200.0;
4172        hi.min_rtt_ms = 200.0;
4173        hi.goodput_bps = 5_000_000.0;
4174        hi.delivery_bps = 5_000_000.0;
4175        hi.avg_paced_frame_bytes = 10_000.0;
4176        hi.avg_preview_frame_bytes = 1_024.0;
4177
4178        let lo_win = target_frame_window(&lo);
4179        let hi_win = target_frame_window(&hi);
4180        assert!(
4181            hi_win > lo_win,
4182            "high-latency link ({hi_win}f) should need more frames in flight \
4183             than low-latency ({lo_win}f)"
4184        );
4185    }
4186
4187    // ── property: small-frame byte window allows pipelining ──
4188
4189    #[test]
4190    fn small_frame_byte_window_enables_pipelining() {
4191        // Tiny terminal frames (~1KB) with a stale congested RTT and low
4192        // goodput estimate (stop-and-wait artifact): byte window must be at
4193        // least target_frame_window × frame_bytes so the sender can pipeline
4194        // rather than stay stuck in stop-and-wait.
4195        let mut client = test_client();
4196        client.display_fps = 120.0;
4197        client.rtt_ms = 165.0;
4198        client.min_rtt_ms = 8.0;
4199        client.goodput_bps = 11_000.0; // stop-and-wait artifact
4200        client.delivery_bps = 6_800.0;
4201        client.last_goodput_sample_bps = 11_000.0;
4202        client.avg_paced_frame_bytes = 1_120.0;
4203        client.avg_preview_frame_bytes = 1_024.0;
4204        client.goodput_jitter_bps = 4_300.0;
4205        client.max_goodput_jitter_bps = 6_500.0;
4206
4207        let window = target_byte_window(&client);
4208        let frames = target_frame_window(&client);
4209        let pipeline = frames * 1_120;
4210
4211        assert!(
4212            window >= pipeline,
4213            "byte window {window}B should be >= pipeline ({frames}f × 1120B = {pipeline}B) \
4214             so small frames can pipeline across the RTT"
4215        );
4216    }
4217
4218    #[test]
4219    fn large_frame_byte_window_bounded_by_one_frame_floor() {
4220        // With large frames (50KB), pipelining the full frame window (5×50KB=250KB)
4221        // would be many multiples of BDP.  Byte window should fall back to
4222        // the one-frame floor so the BDP budget governs.
4223        let mut client = test_client();
4224        client.display_fps = 120.0;
4225        client.rtt_ms = 165.0;
4226        client.min_rtt_ms = 8.0;
4227        client.goodput_bps = 11_000.0;
4228        client.delivery_bps = 6_800.0;
4229        client.last_goodput_sample_bps = 11_000.0;
4230        client.avg_paced_frame_bytes = 50_000.0; // large frame
4231        client.avg_preview_frame_bytes = 1_024.0;
4232        client.goodput_jitter_bps = 0.0;
4233        client.max_goodput_jitter_bps = 0.0;
4234
4235        let window = target_byte_window(&client);
4236        let frames = target_frame_window(&client);
4237        let pipeline = frames.saturating_mul(50_000);
4238
4239        assert!(
4240            window < pipeline,
4241            "byte window {window}B should be < full pipeline {pipeline}B \
4242             ({frames}f × 50KB) — large frames must use one-frame floor"
4243        );
4244        assert!(
4245            window >= 50_000,
4246            "byte window {window}B must be at least one frame (50KB)"
4247        );
4248    }
4249
4250    // ── property: preview reservation applies uniformly ──
4251
4252    #[test]
4253    fn preview_reservation_applies_even_on_low_latency_high_bandwidth_links() {
4254        let mut client = browser_ready_high_bandwidth_client();
4255        client.lead = Some(1);
4256        client.subscriptions.insert(1);
4257        let target = target_frame_window(&client);
4258        fill_inflight(&mut client, target.saturating_sub(1), 512);
4259        assert!(
4260            !lead_window_open(&client, true),
4261            "preview reservation should apply uniformly for lead clients"
4262        );
4263    }
4264
4265    // ── property: blip recovery on healthy paths ──
4266
4267    #[test]
4268    fn probe_recovers_on_healthy_path_after_blip() {
4269        let mut client = browser_ready_high_bandwidth_client();
4270        client.probe_frames = 8.0;
4271
4272        // Blip: 3 ACKs with inflated RTT crush probes.
4273        sim_acks(&mut client, 3, 30_000, 200.0);
4274        let post_blip = client.probe_frames;
4275        assert!(
4276            post_blip < 4.0,
4277            "probe_frames {post_blip:.1} should have dropped after blip"
4278        );
4279
4280        // Reset browser metrics to healthy (browser cleared backlog).
4281        client.browser_backlog_frames = 0;
4282        client.browser_ack_ahead_frames = 0;
4283        client.browser_apply_ms = 0.3;
4284
4285        // Recovery: 20 healthy ACKs at low RTT on an underfilled path.
4286        sim_acks(&mut client, 20, 30_000, 1.0);
4287
4288        assert!(
4289            client.probe_frames > post_blip,
4290            "probe_frames {:.1} should have recovered from {post_blip:.1} after healthy ACKs",
4291            client.probe_frames,
4292        );
4293    }
4294
4295    #[test]
4296    fn jitter_decays_fast_on_browser_ready_path() {
4297        let mut client = browser_ready_high_bandwidth_client();
4298
4299        // Inject elevated jitter (simulating post-blip state).
4300        client.max_goodput_jitter_bps = client.goodput_bps * 0.4;
4301        client.goodput_jitter_bps = client.goodput_bps * 0.3;
4302        let initial_jitter = client.max_goodput_jitter_bps;
4303
4304        // 10 healthy ACKs on a browser-ready path.
4305        sim_acks(&mut client, 10, 30_000, 1.0);
4306
4307        assert!(
4308            client.max_goodput_jitter_bps < initial_jitter * 0.5,
4309            "max_goodput_jitter_bps {:.0} should have decayed below {:.0} \
4310             (50% of initial {initial_jitter:.0}) after 10 healthy ACKs on a ready path",
4311            client.max_goodput_jitter_bps,
4312            initial_jitter * 0.5,
4313        );
4314    }
4315
4316    #[test]
4317    fn byte_budget_uses_floor_when_goodput_depressed() {
4318        let mut client = browser_ready_high_bandwidth_client();
4319        client.goodput_bps = 100_000.0;
4320
4321        let budget = byte_budget_for(&client, 100.0);
4322        let floor_budget = (bandwidth_floor_bps(&client) * 100.0 / 1_000.0).ceil() as usize;
4323
4324        assert!(
4325            budget >= floor_budget,
4326            "byte_budget {budget} should be at least bandwidth_floor-based {floor_budget} \
4327             when goodput_bps is depressed but delivery_bps is high"
4328        );
4329    }
4330
4331    #[test]
4332    fn probe_floor_maintained_under_congestion_signal() {
4333        let mut client = test_client();
4334        client.display_fps = 120.0;
4335        client.rtt_ms = 40.0;
4336        client.min_rtt_ms = 40.0;
4337        client.goodput_bps = 5_000_000.0;
4338        client.delivery_bps = 5_000_000.0;
4339        client.last_goodput_sample_bps = 5_000_000.0;
4340        client.avg_paced_frame_bytes = 10_000.0;
4341        client.avg_preview_frame_bytes = 1_024.0;
4342        client.probe_frames = 10.0;
4343
4344        // Many ACKs with high RTT: probes should not drop below the floor.
4345        sim_acks(&mut client, 20, 10_000, 600.0);
4346
4347        assert!(
4348            client.probe_frames >= 1.0,
4349            "probe_frames {:.1} should not drop below the floor of 1.0",
4350            client.probe_frames,
4351        );
4352    }
4353}