Skip to main content

stream_line_bridge/
stream_line_bridge.rs

1use std::env;
2use std::error::Error;
3use std::fs;
4use std::io::{self, Write};
5
6use highlight_spans::{Grammar, SpanHighlighter};
7use render_ansi::{ColorMode, StreamLineRenderer};
8use theme_engine::load_theme;
9
10#[derive(Debug, Clone)]
11struct Options {
12    source_path: String,
13    theme_name: String,
14    grammar_name: String,
15    color_mode: ColorMode,
16    previous_source_path: Option<String>,
17}
18
19/// Parses a grammar argument and returns a human-friendly error on failure.
20fn parse_grammar(input: &str) -> Result<Grammar, String> {
21    Grammar::from_name(input).ok_or_else(|| {
22        format!(
23            "unknown grammar '{}'; use one of: {}",
24            input,
25            Grammar::supported_names().join(", ")
26        )
27    })
28}
29
30/// Parses a color mode argument and returns a human-friendly error on failure.
31fn parse_color_mode(input: &str) -> Result<ColorMode, String> {
32    ColorMode::from_name(input).ok_or_else(|| {
33        format!(
34            "unknown color mode '{}'; use one of: {}",
35            input,
36            ColorMode::supported_names().join(", ")
37        )
38    })
39}
40
41/// Parses CLI options for the `stream_line_bridge` example.
42///
43/// Returns a user-facing error string suitable for usage output.
44fn parse_args(args: &[String]) -> Result<Options, String> {
45    if args.len() < 2 {
46        return Err("missing source file".to_string());
47    }
48
49    let source_path = args[1].clone();
50    let theme_name = args
51        .get(2)
52        .cloned()
53        .unwrap_or_else(|| "tokyonight-dark".to_string());
54    let grammar_name = args
55        .get(3)
56        .cloned()
57        .unwrap_or_else(|| "objectscript".to_string());
58
59    let mut color_mode = ColorMode::TrueColor;
60    let mut previous_source_path: Option<String> = None;
61    let mut i = 4usize;
62    while i < args.len() {
63        match args[i].as_str() {
64            "--prev" => {
65                i += 1;
66                let Some(value) = args.get(i) else {
67                    return Err("expected value after --prev".to_string());
68                };
69                previous_source_path = Some(value.clone());
70            }
71            "--color-mode" => {
72                i += 1;
73                let Some(value) = args.get(i) else {
74                    return Err("expected value after --color-mode".to_string());
75                };
76                color_mode = parse_color_mode(value)?;
77            }
78            flag => return Err(format!("unknown option '{flag}'")),
79        }
80        i += 1;
81    }
82
83    Ok(Options {
84        source_path,
85        theme_name,
86        grammar_name,
87        color_mode,
88        previous_source_path,
89    })
90}
91
92/// Prints CLI usage for the `stream_line_bridge` example.
93fn print_usage() {
94    eprintln!("Usage:");
95    eprintln!(
96        "  cargo run -p render-ansi --example stream_line_bridge -- <source-file> [theme] [grammar] [--color-mode truecolor|ansi256|ansi16] [--prev <old-source-file>]"
97    );
98    eprintln!();
99    eprintln!("Notes:");
100    eprintln!("  - Inputs must be single-line text files (no newline).");
101    eprintln!();
102    eprintln!("Examples:");
103    eprintln!("  cargo run -p render-ansi --example stream_line_bridge -- new.sql");
104    eprintln!(
105        "  cargo run -p render-ansi --example stream_line_bridge -- new.sql tokyonight-dark sql --prev old.sql"
106    );
107    eprintln!(
108        "  cargo run -p render-ansi --example stream_line_bridge -- new.sql tokyonight-dark sql --color-mode ansi16"
109    );
110}
111
112/// Emits a stream-safe single-line VT patch for highlighted source.
113///
114/// When `--prev` is provided, the previous file seeds renderer state so output
115/// contains only the delta from previous to current content.
116///
117/// # Errors
118///
119/// Returns an error when file IO, theme loading, or highlighting fails.
120fn main() -> Result<(), Box<dyn Error>> {
121    let args: Vec<String> = env::args().collect();
122    let options = match parse_args(&args) {
123        Ok(options) => options,
124        Err(err) => {
125            eprintln!("invalid arguments: {err}");
126            eprintln!();
127            print_usage();
128            return Ok(());
129        }
130    };
131
132    let grammar =
133        parse_grammar(&options.grammar_name).map_err(|msg| format!("invalid grammar: {msg}"))?;
134    let source = fs::read(&options.source_path)?;
135    let theme = load_theme(&options.theme_name)?;
136    let mut highlighter = SpanHighlighter::new()?;
137    let mut renderer = StreamLineRenderer::new();
138    renderer.set_color_mode(options.color_mode);
139
140    if let Some(previous_source_path) = &options.previous_source_path {
141        let previous_source = fs::read(previous_source_path)?;
142        let _ = renderer.highlight_line_to_patch(
143            &mut highlighter,
144            &previous_source,
145            grammar,
146            &theme,
147        )?;
148    }
149
150    let patch = renderer.highlight_line_to_patch(&mut highlighter, &source, grammar, &theme)?;
151    print!("{patch}");
152    io::stdout().flush()?;
153
154    Ok(())
155}