Skip to main content

tui_test/
logger.rs

1//! Optional verbose logging of everything a terminal session reads and writes
2//! the PTY, plus lifecycle events. Modeled on inshellisense's data log: every
3//! record is timestamped and byte-escaped so control sequences are visible.
4//!
5//! Tags:
6//!   READ   bytes read from the PTY (what the app draws on screen)
7//!   WRITE  bytes written to the PTY (keystrokes / input)
8//!   REPLY  bytes we wrote back to the PTY answering a capability query
9//!   EVENT  lifecycle notes (open, resize, exit, requests)
10
11use std::fs::OpenOptions;
12use std::io::{BufWriter, Write};
13use std::path::Path;
14use std::sync::Mutex;
15use std::time::{SystemTime, UNIX_EPOCH};
16
17pub struct Logger {
18    sink: Option<Mutex<BufWriter<std::fs::File>>>,
19}
20
21impl Logger {
22    /// A no-op logger.
23    pub fn disabled() -> Self {
24        Logger { sink: None }
25    }
26
27    /// Create a logger that truncates and writes to `path`.
28    pub fn to_file(path: &Path) -> std::io::Result<Self> {
29        let file = OpenOptions::new()
30            .create(true)
31            .write(true)
32            .truncate(true)
33            .open(path)?;
34        Ok(Logger {
35            sink: Some(Mutex::new(BufWriter::new(file))),
36        })
37    }
38
39    pub fn enabled(&self) -> bool {
40        self.sink.is_some()
41    }
42
43    pub fn event(&self, msg: &str) {
44        self.write_line("EVENT", msg);
45    }
46
47    pub fn read(&self, bytes: &[u8]) {
48        if self.sink.is_some() {
49            self.write_line("READ ", &escape(bytes));
50        }
51    }
52
53    pub fn write(&self, bytes: &[u8]) {
54        if self.sink.is_some() {
55            self.write_line("WRITE", &escape(bytes));
56        }
57    }
58
59    pub fn reply(&self, bytes: &[u8]) {
60        if self.sink.is_some() {
61            self.write_line("REPLY", &escape(bytes));
62        }
63    }
64
65    fn write_line(&self, tag: &str, body: &str) {
66        let Some(sink) = &self.sink else { return };
67        if let Ok(mut w) = sink.lock() {
68            let _ = writeln!(w, "{} {tag} {body}", timestamp());
69            let _ = w.flush();
70        }
71    }
72}
73
74/// UTC time-of-day as `HH:MM:SS.mmm`.
75fn timestamp() -> String {
76    let now = SystemTime::now()
77        .duration_since(UNIX_EPOCH)
78        .unwrap_or_default();
79    let secs = now.as_secs();
80    let millis = now.subsec_millis();
81    let h = (secs / 3600) % 24;
82    let m = (secs / 60) % 60;
83    let s = secs % 60;
84    format!("{h:02}:{m:02}:{s:02}.{millis:03}")
85}
86
87/// Render bytes with control characters made visible.
88fn escape(bytes: &[u8]) -> String {
89    let mut out = String::with_capacity(bytes.len());
90    for &b in bytes {
91        match b {
92            b'\\' => out.push_str("\\\\"),
93            0x1b => out.push_str("\\e"),
94            b'\n' => out.push_str("\\n"),
95            b'\r' => out.push_str("\\r"),
96            b'\t' => out.push_str("\\t"),
97            0x20..=0x7e => out.push(b as char),
98            _ => out.push_str(&format!("\\x{b:02x}")),
99        }
100    }
101    out
102}