vt_patch_bridge/
vt_patch_bridge.rs1use 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
23fn 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
34fn 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
50fn 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
61fn 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
148fn 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
174fn 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}