agentmux_srv/backend/viewer/
terminal.rs1pub const SNAPSHOT_TAIL_BYTES: u64 = 256 * 1024;
18const LINE_START_WINDOW: usize = 64 * 1024;
20
21pub fn tail_start(offset: u64, data: &[u8]) -> usize {
26 if offset == 0 {
27 return 0;
28 }
29 let window = &data[..data.len().min(LINE_START_WINDOW)];
30 window.iter().position(|b| *b == b'\n').map_or(0, |i| i + 1)
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum TermStep {
36 Send { offset: u64, data: Vec<u8> },
38 Skip,
40 Resync(&'static str),
42}
43
44pub fn apply_term_append(next: &mut u64, offset: Option<u64>, data: &[u8]) -> TermStep {
46 let Some(offset) = offset else {
47 return TermStep::Resync("offset");
49 };
50 let end = offset + data.len() as u64;
51 if end <= *next {
52 return TermStep::Skip;
53 }
54 if offset > *next {
55 return TermStep::Resync("gap");
56 }
57 let skip = (*next - offset) as usize;
58 let at = *next;
59 *next = end;
60 TermStep::Send { offset: at, data: data[skip..].to_vec() }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 #[test]
68 fn a_tail_starts_after_its_first_newline_unless_it_starts_the_file() {
69 assert_eq!(tail_start(0, b"\x1b[1mbold\nnext"), 0, "the file's start");
70 assert_eq!(tail_start(10, b"[1mbold\nnext\n"), 8, "mid escape: from the next line");
71 assert_eq!(tail_start(10, b"no newline at all"), 0);
72 let mut late = vec![b'x'; LINE_START_WINDOW];
73 late.extend_from_slice(b"\nrest");
74 assert_eq!(tail_start(10, &late), 0, "a newline past the window isn't looked for");
75 }
76
77 #[test]
78 fn appends_continue_the_cursor_and_drop_what_was_sent() {
79 let mut next = 10;
80 assert_eq!(apply_term_append(&mut next, Some(10), b"abc"), TermStep::Send { offset: 10, data: b"abc".to_vec() });
81 assert_eq!(next, 13);
82 assert_eq!(apply_term_append(&mut next, Some(8), b"xyz"), TermStep::Skip, "ends at the cursor");
83 assert_eq!(apply_term_append(&mut next, Some(11), b"cdef"), TermStep::Send { offset: 13, data: b"ef".to_vec() });
84 assert_eq!(next, 15);
85 }
86
87 #[test]
88 fn a_gap_or_an_unplaced_append_resyncs() {
89 let mut next = 10;
90 assert_eq!(apply_term_append(&mut next, Some(11), b"x"), TermStep::Resync("gap"));
91 assert_eq!(apply_term_append(&mut next, None, b"x"), TermStep::Resync("offset"));
92 assert_eq!(next, 10, "nothing moved");
93 }
94}