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