calepin 0.0.19

A Rust CLI for preprocessing Typst documents with executable code chunks
use std::io::{self, BufRead, BufReader, Read, Write};
use std::thread;

pub(super) fn relay_typst_watch_output<R, W>(reader: R, writer: W) -> io::Result<()>
where
    R: Read,
    W: Write,
{
    let mut reader = BufReader::new(reader);
    let mut relay = TypstWatchOutputRelay::new(writer);
    let mut line = String::new();

    loop {
        line.clear();
        let bytes = reader.read_line(&mut line)?;
        if bytes == 0 {
            break;
        }
        relay.write_line(&line)?;
    }

    relay.finish()
}

struct TypstWatchOutputRelay<W> {
    writer: W,
    seen_watching: bool,
    seen_writing: bool,
    pending_blank: bool,
}

impl<W: Write> TypstWatchOutputRelay<W> {
    fn new(writer: W) -> Self {
        Self {
            writer,
            seen_watching: false,
            seen_writing: false,
            pending_blank: false,
        }
    }

    fn write_line(&mut self, line: &str) -> io::Result<()> {
        let line = line.trim_end_matches(['\r', '\n']);
        if line.trim().is_empty() {
            self.pending_blank = true;
            return Ok(());
        }

        let should_write = match typst_watch_line(line) {
            TypstWatchLine::Watching => {
                let first = !self.seen_watching;
                self.seen_watching = true;
                first
            }
            TypstWatchLine::Writing => {
                let first = !self.seen_writing;
                self.seen_writing = true;
                first
            }
            TypstWatchLine::Status => true,
            TypstWatchLine::Other => {
                if self.pending_blank {
                    writeln!(self.writer)?;
                }
                true
            }
        };
        self.pending_blank = false;

        if should_write {
            writeln!(self.writer, "{line}")?;
        }
        Ok(())
    }

    fn finish(mut self) -> io::Result<()> {
        self.writer.flush()
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TypstWatchLine {
    Watching,
    Writing,
    Status,
    Other,
}

fn typst_watch_line(line: &str) -> TypstWatchLine {
    if line.starts_with("watching ") {
        TypstWatchLine::Watching
    } else if line.starts_with("writing to ") {
        TypstWatchLine::Writing
    } else if line.starts_with('[') && line.contains("] ") {
        TypstWatchLine::Status
    } else {
        TypstWatchLine::Other
    }
}

pub(super) fn join_relay(name: &str, handle: thread::JoinHandle<io::Result<()>>) {
    match handle.join() {
        Ok(Ok(())) => {}
        Ok(Err(error)) => {
            cwarn!("failed to relay typst watch {name}: {}", error);
        }
        Err(_) => {
            cwarn!("typst watch {name} relay panicked");
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn relay_typst_watch_output_compacts_status_lines() {
        let input = "\
watching example.typ
writing to example.pdf

[10:17:43] compiling ...

watching example.typ
writing to example.pdf

[10:17:43] compiled successfully in 88.22 ms

watching example.typ
writing to example.pdf

[10:17:53] compiling ...
";
        let mut output = Vec::new();

        relay_typst_watch_output(input.as_bytes(), &mut output).unwrap();

        assert_eq!(
            String::from_utf8(output).unwrap(),
            "\
watching example.typ
writing to example.pdf
[10:17:43] compiling ...
[10:17:43] compiled successfully in 88.22 ms
[10:17:53] compiling ...
"
        );
    }

    #[test]
    fn relay_typst_watch_output_preserves_diagnostic_spacing() {
        let input = "\
error: failed

hint: check this
";
        let mut output = Vec::new();

        relay_typst_watch_output(input.as_bytes(), &mut output).unwrap();

        assert_eq!(
            String::from_utf8(output).unwrap(),
            "\
error: failed

hint: check this
"
        );
    }
}