Skip to main content

vt_patch_bridge/
vt_patch_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, IncrementalRenderer};
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    width: usize,
16    height: usize,
17    origin_row: usize,
18    origin_col: usize,
19    color_mode: ColorMode,
20    previous_source_path: Option<String>,
21}
22
23/// Parses a grammar argument and returns a human-friendly error on failure.
24fn parse_grammar(input: &str) -> Result<Grammar, String> {
25    Grammar::from_name(input).ok_or_else(|| {
26        format!(
27            "unknown grammar '{}'; use one of: {}",
28            input,
29            Grammar::supported_names().join(", ")
30        )
31    })
32}
33
34/// Parses a viewport dimension from CLI value, env var, or fallback.
35///
36/// The resolved value is always at least `1`.
37fn parse_dimension(value: Option<&str>, env_key: &str, fallback: usize) -> usize {
38    if let Some(v) = value {
39        if let Ok(parsed) = v.parse::<usize>() {
40            return parsed.max(1);
41        }
42    }
43    env::var(env_key)
44        .ok()
45        .and_then(|s| s.parse::<usize>().ok())
46        .map(|v| v.max(1))
47        .unwrap_or(fallback)
48}
49
50/// Parses a color mode argument and returns a human-friendly error on failure.
51fn parse_color_mode(input: &str) -> Result<ColorMode, String> {
52    ColorMode::from_name(input).ok_or_else(|| {
53        format!(
54            "unknown color mode '{}'; use one of: {}",
55            input,
56            ColorMode::supported_names().join(", ")
57        )
58    })
59}
60
61/// Parses CLI options for the `vt_patch_bridge` example.
62///
63/// Returns a user-facing error string suitable for usage output.
64fn parse_args(args: &[String]) -> Result<Options, String> {
65    if args.len() < 2 {
66        return Err("missing source file".to_string());
67    }
68
69    let source_path = args[1].clone();
70    let theme_name = args
71        .get(2)
72        .cloned()
73        .unwrap_or_else(|| "tokyonight-dark".to_string());
74    let grammar_name = args
75        .get(3)
76        .cloned()
77        .unwrap_or_else(|| "objectscript".to_string());
78
79    let mut width_arg: Option<String> = None;
80    let mut height_arg: Option<String> = None;
81    let mut origin_row_arg: Option<String> = None;
82    let mut origin_col_arg: Option<String> = None;
83    let mut color_mode = ColorMode::TrueColor;
84    let mut previous_source_path: Option<String> = None;
85    let mut i = 4usize;
86    while i < args.len() {
87        match args[i].as_str() {
88            "--width" => {
89                i += 1;
90                let Some(value) = args.get(i) else {
91                    return Err("expected value after --width".to_string());
92                };
93                width_arg = Some(value.clone());
94            }
95            "--height" => {
96                i += 1;
97                let Some(value) = args.get(i) else {
98                    return Err("expected value after --height".to_string());
99                };
100                height_arg = Some(value.clone());
101            }
102            "--origin-row" => {
103                i += 1;
104                let Some(value) = args.get(i) else {
105                    return Err("expected value after --origin-row".to_string());
106                };
107                origin_row_arg = Some(value.clone());
108            }
109            "--origin-col" => {
110                i += 1;
111                let Some(value) = args.get(i) else {
112                    return Err("expected value after --origin-col".to_string());
113                };
114                origin_col_arg = Some(value.clone());
115            }
116            "--prev" => {
117                i += 1;
118                let Some(value) = args.get(i) else {
119                    return Err("expected value after --prev".to_string());
120                };
121                previous_source_path = Some(value.clone());
122            }
123            "--color-mode" => {
124                i += 1;
125                let Some(value) = args.get(i) else {
126                    return Err("expected value after --color-mode".to_string());
127                };
128                color_mode = parse_color_mode(value)?;
129            }
130            flag => return Err(format!("unknown option '{flag}'")),
131        }
132        i += 1;
133    }
134
135    Ok(Options {
136        source_path,
137        theme_name,
138        grammar_name,
139        width: parse_dimension(width_arg.as_deref(), "COLUMNS", 240),
140        height: parse_dimension(height_arg.as_deref(), "LINES", 80),
141        origin_row: parse_dimension(origin_row_arg.as_deref(), "ORIGIN_ROW", 1),
142        origin_col: parse_dimension(origin_col_arg.as_deref(), "ORIGIN_COL", 1),
143        color_mode,
144        previous_source_path,
145    })
146}
147
148/// Prints CLI usage for the `vt_patch_bridge` example.
149fn print_usage() {
150    eprintln!("Usage:");
151    eprintln!(
152        "  cargo run -p render-ansi --example vt_patch_bridge -- <source-file> [theme] [grammar] [--width N] [--height N] [--origin-row N] [--origin-col N] [--color-mode truecolor|ansi256|ansi16] [--prev <old-source-file>]"
153    );
154    eprintln!();
155    eprintln!("Examples:");
156    eprintln!("  cargo run -p render-ansi --example vt_patch_bridge -- sample.cls");
157    eprintln!(
158        "  cargo run -p render-ansi --example vt_patch_bridge -- sample.mac solarized-dark objectscript --width 200 --height 60"
159    );
160    eprintln!(
161        "  cargo run -p render-ansi --example vt_patch_bridge -- sample.mac tokyonight-dark objectscript --origin-row 4 --origin-col 7"
162    );
163    eprintln!(
164        "  cargo run -p render-ansi --example vt_patch_bridge -- new.mac tokyonight-dark objectscript --prev old.mac"
165    );
166    eprintln!(
167        "  cargo run -p render-ansi --example vt_patch_bridge -- sample.sql tokyonight-dark sql --color-mode ansi256"
168    );
169    eprintln!(
170        "  cargo run -p render-ansi --example vt_patch_bridge -- sample.sql tokyonight-dark sql --color-mode ansi16"
171    );
172}
173
174/// Emits an incremental VT patch for a highlighted source file.
175///
176/// When `--prev` is provided, the previous file seeds renderer state so output
177/// contains only the delta from previous to current content.
178///
179/// # Errors
180///
181/// Returns an error when file IO, theme loading, or highlighting fails.
182fn main() -> Result<(), Box<dyn Error>> {
183    let args: Vec<String> = env::args().collect();
184    let options = match parse_args(&args) {
185        Ok(options) => options,
186        Err(err) => {
187            eprintln!("invalid arguments: {err}");
188            eprintln!();
189            print_usage();
190            return Ok(());
191        }
192    };
193
194    let grammar =
195        parse_grammar(&options.grammar_name).map_err(|msg| format!("invalid grammar: {msg}"))?;
196    let source = fs::read(&options.source_path)?;
197    let theme = load_theme(&options.theme_name)?;
198    let mut highlighter = SpanHighlighter::new()?;
199    let mut renderer = IncrementalRenderer::new(options.width, options.height);
200    renderer.set_origin(options.origin_row, options.origin_col);
201    renderer.set_color_mode(options.color_mode);
202
203    if let Some(previous_source_path) = &options.previous_source_path {
204        let previous_source = fs::read(previous_source_path)?;
205        let _ = renderer.highlight_to_patch(&mut highlighter, &previous_source, grammar, &theme)?;
206    }
207
208    let patch = renderer.highlight_to_patch(&mut highlighter, &source, grammar, &theme)?;
209    print!("{patch}");
210    io::stdout().flush()?;
211
212    Ok(())
213}