Skip to main content

pacenotes_installer\runlog/
run_log.rs

1pub struct Log {
2    header: Vec<String>,
3    entries: Vec<Entry>,
4    clock: fn() -> i64,
5    path: Option<std::path::PathBuf>,
6    file: Option<std::fs::File>,
7}
8
9/// A line, and the moment it was said, which a line replayed into the file
10/// later must keep.
11struct Entry {
12    said_at: i64,
13    text: String,
14}
15
16impl Log {
17    /// `header` heads the file and not the pane: it is what support asks for
18    /// first, which a user reading along does not need. A file that cannot be
19    /// opened leaves a log without one.
20    pub fn create(header: Vec<String>, path: Option<std::path::PathBuf>) -> Log {
21        Log::with_clock(header, path, crate::utc_time::unix_now)
22    }
23
24    /// `create`, telling the time by `clock`, so a test can name the moment.
25    pub fn with_clock(
26        header: Vec<String>,
27        path: Option<std::path::PathBuf>,
28        clock: fn() -> i64,
29    ) -> Log {
30        let mut log = Log {
31            header,
32            entries: Vec::new(),
33            clock,
34            path: None,
35            file: None,
36        };
37        if let Some(path) = path {
38            log.attach(path);
39        }
40        log
41    }
42
43    /// Points the log at a file, writing out the header and everything said so
44    /// far, since the game folder it belongs in is only known partway through
45    /// the run; a second call follows the run to another folder.
46    pub fn attach(&mut self, path: std::path::PathBuf) {
47        if let Some(parent) = path.parent() {
48            let _ = std::fs::create_dir_all(parent);
49            super::prune(parent, super::KEEP_LOGS.saturating_sub(1));
50        }
51        let Ok(mut file) = std::fs::File::create(&path) else {
52            return;
53        };
54        let lines = self
55            .header
56            .iter()
57            .cloned()
58            .chain(std::iter::once(String::new()))
59            .chain(self.entries.iter().map(stamped));
60        for line in lines {
61            let _ = std::io::Write::write_all(&mut file, format!("{line}\n").as_bytes());
62        }
63        let _ = std::io::Write::flush(&mut file);
64        self.file = Some(file);
65        self.path = Some(path);
66    }
67
68    /// Flushed at once, so a run that dies still leaves the line saying what it was doing.
69    pub fn line(&mut self, text: impl Into<String>) {
70        let entry = Entry {
71            said_at: (self.clock)(),
72            text: text.into(),
73        };
74        if let Some(file) = self.file.as_mut() {
75            let _ = std::io::Write::write_all(file, format!("{}\n", stamped(&entry)).as_bytes());
76            let _ = std::io::Write::flush(file);
77        }
78        self.entries.push(entry);
79    }
80
81    /// As the pane shows them: without the times, which only the file needs.
82    pub fn lines(&self) -> impl Iterator<Item = &str> {
83        self.entries.iter().map(|entry| entry.text.as_str())
84    }
85
86    /// CRLF-separated, because a Win32 multiline edit ignores a lone LF.
87    pub fn text(&self) -> String {
88        self.lines().collect::<Vec<_>>().join("\r\n")
89    }
90
91    /// `None` when no file was written, so the summary never points at one that does not exist.
92    pub fn path(&self) -> Option<&std::path::Path> {
93        self.path.as_deref()
94    }
95
96    pub fn is_empty(&self) -> bool {
97        self.entries.is_empty()
98    }
99}
100
101/// UTC in the server log's own shape, so a user's log lines up with the server's.
102fn stamped(entry: &Entry) -> String {
103    format!(
104        "{}  {}",
105        crate::utc_time::iso_stamp(entry.said_at),
106        entry.text
107    )
108}