11use clap:: Parser ;
2+ use crate :: theme:: ThemeMode ;
23
34/// A terminal diff viewer with AI-powered semantic grouping.
45///
@@ -16,11 +17,27 @@ use clap::Parser;
1617#[ derive( Parser , Debug ) ]
1718#[ command( name = "semantic-diff" , version, about) ]
1819pub struct Cli {
20+ /// Color theme: auto, dark, or light. Auto-detects terminal background.
21+ /// Can also be set via SEMANTIC_DIFF_THEME env var.
22+ /// Use --theme=light for SSH/tmux sessions where auto-detection fails.
23+ #[ arg( long, value_name = "MODE" , env = "SEMANTIC_DIFF_THEME" ) ]
24+ pub theme : Option < String > ,
25+
1926 /// Arguments passed through to `git diff` (commits, ranges, --staged, -- paths, etc.)
2027 #[ arg( trailing_var_arg = true , allow_hyphen_values = true ) ]
2128 pub git_args : Vec < String > ,
2229}
2330
31+ impl Cli {
32+ pub fn theme_mode ( & self ) -> ThemeMode {
33+ match self . theme . as_deref ( ) {
34+ Some ( "dark" ) => ThemeMode :: Dark ,
35+ Some ( "light" ) => ThemeMode :: Light ,
36+ _ => ThemeMode :: Auto ,
37+ }
38+ }
39+ }
40+
2441impl Cli {
2542 /// Build the full argument list for `git diff`, prepending `-M` for rename detection.
2643 pub fn git_diff_args ( & self ) -> Vec < String > {
@@ -36,13 +53,14 @@ mod tests {
3653
3754 #[ test]
3855 fn test_no_args_produces_bare_diff ( ) {
39- let cli = Cli { git_args : vec ! [ ] } ;
56+ let cli = Cli { theme : None , git_args : vec ! [ ] } ;
4057 assert_eq ! ( cli. git_diff_args( ) , vec![ "diff" , "-M" ] ) ;
4158 }
4259
4360 #[ test]
4461 fn test_head_arg ( ) {
4562 let cli = Cli {
63+ theme : None ,
4664 git_args : vec ! [ "HEAD" . to_string( ) ] ,
4765 } ;
4866 assert_eq ! ( cli. git_diff_args( ) , vec![ "diff" , "-M" , "HEAD" ] ) ;
@@ -51,6 +69,7 @@ mod tests {
5169 #[ test]
5270 fn test_staged_flag ( ) {
5371 let cli = Cli {
72+ theme : None ,
5473 git_args : vec ! [ "--staged" . to_string( ) ] ,
5574 } ;
5675 assert_eq ! ( cli. git_diff_args( ) , vec![ "diff" , "-M" , "--staged" ] ) ;
@@ -59,6 +78,7 @@ mod tests {
5978 #[ test]
6079 fn test_two_dot_range ( ) {
6180 let cli = Cli {
81+ theme : None ,
6282 git_args : vec ! [ "main..feature" . to_string( ) ] ,
6383 } ;
6484 assert_eq ! ( cli. git_diff_args( ) , vec![ "diff" , "-M" , "main..feature" ] ) ;
@@ -67,6 +87,7 @@ mod tests {
6787 #[ test]
6888 fn test_three_dot_range ( ) {
6989 let cli = Cli {
90+ theme : None ,
7091 git_args : vec ! [ "main...feature" . to_string( ) ] ,
7192 } ;
7293 assert_eq ! ( cli. git_diff_args( ) , vec![ "diff" , "-M" , "main...feature" ] ) ;
@@ -75,6 +96,7 @@ mod tests {
7596 #[ test]
7697 fn test_two_refs ( ) {
7798 let cli = Cli {
99+ theme : None ,
78100 git_args : vec ! [ "main" . to_string( ) , "feature" . to_string( ) ] ,
79101 } ;
80102 assert_eq ! (
@@ -86,6 +108,7 @@ mod tests {
86108 #[ test]
87109 fn test_path_limiter ( ) {
88110 let cli = Cli {
111+ theme : None ,
89112 git_args : vec ! [
90113 "HEAD" . to_string( ) ,
91114 "--" . to_string( ) ,
@@ -101,6 +124,7 @@ mod tests {
101124 #[ test]
102125 fn test_cached_alias ( ) {
103126 let cli = Cli {
127+ theme : None ,
104128 git_args : vec ! [ "--cached" . to_string( ) ] ,
105129 } ;
106130 assert_eq ! ( cli. git_diff_args( ) , vec![ "diff" , "-M" , "--cached" ] ) ;
@@ -111,6 +135,7 @@ mod tests {
111135 #[ test]
112136 fn test_head_tilde_syntax ( ) {
113137 let cli = Cli {
138+ theme : None ,
114139 git_args : vec ! [ "HEAD~3" . to_string( ) ] ,
115140 } ;
116141 assert_eq ! ( cli. git_diff_args( ) , vec![ "diff" , "-M" , "HEAD~3" ] ) ;
@@ -119,6 +144,7 @@ mod tests {
119144 #[ test]
120145 fn test_head_caret_syntax ( ) {
121146 let cli = Cli {
147+ theme : None ,
122148 git_args : vec ! [ "HEAD^" . to_string( ) ] ,
123149 } ;
124150 assert_eq ! ( cli. git_diff_args( ) , vec![ "diff" , "-M" , "HEAD^" ] ) ;
@@ -127,6 +153,7 @@ mod tests {
127153 #[ test]
128154 fn test_sha_refs ( ) {
129155 let cli = Cli {
156+ theme : None ,
130157 git_args : vec ! [
131158 "abc1234" . to_string( ) ,
132159 "def5678" . to_string( ) ,
@@ -142,6 +169,7 @@ mod tests {
142169 fn test_full_sha ( ) {
143170 let sha = "a" . repeat ( 40 ) ;
144171 let cli = Cli {
172+ theme : None ,
145173 git_args : vec ! [ sha. clone( ) ] ,
146174 } ;
147175 assert_eq ! ( cli. git_diff_args( ) , vec![ "diff" , "-M" , & sha] ) ;
@@ -150,6 +178,7 @@ mod tests {
150178 #[ test]
151179 fn test_staged_with_ref ( ) {
152180 let cli = Cli {
181+ theme : None ,
153182 git_args : vec ! [ "--staged" . to_string( ) , "HEAD~1" . to_string( ) ] ,
154183 } ;
155184 assert_eq ! (
@@ -161,6 +190,7 @@ mod tests {
161190 #[ test]
162191 fn test_multiple_path_limiters ( ) {
163192 let cli = Cli {
193+ theme : None ,
164194 git_args : vec ! [
165195 "HEAD" . to_string( ) ,
166196 "--" . to_string( ) ,
@@ -178,6 +208,7 @@ mod tests {
178208 #[ test]
179209 fn test_two_dot_range_with_paths ( ) {
180210 let cli = Cli {
211+ theme : None ,
181212 git_args : vec ! [
182213 "main..feature" . to_string( ) ,
183214 "--" . to_string( ) ,
@@ -193,6 +224,7 @@ mod tests {
193224 #[ test]
194225 fn test_three_dot_range_with_paths ( ) {
195226 let cli = Cli {
227+ theme : None ,
196228 git_args : vec ! [
197229 "origin/main...HEAD" . to_string( ) ,
198230 "--" . to_string( ) ,
@@ -208,6 +240,7 @@ mod tests {
208240 #[ test]
209241 fn test_merge_base_flag ( ) {
210242 let cli = Cli {
243+ theme : None ,
211244 git_args : vec ! [ "--merge-base" . to_string( ) , "main" . to_string( ) ] ,
212245 } ;
213246 assert_eq ! (
@@ -219,6 +252,7 @@ mod tests {
219252 #[ test]
220253 fn test_no_index_flag ( ) {
221254 let cli = Cli {
255+ theme : None ,
222256 git_args : vec ! [
223257 "--no-index" . to_string( ) ,
224258 "file_a.txt" . to_string( ) ,
@@ -235,6 +269,7 @@ mod tests {
235269 fn test_many_positional_args_stress ( ) {
236270 let args: Vec < String > = ( 0 ..100 ) . map ( |i| format ! ( "path_{i}.rs" ) ) . collect ( ) ;
237271 let cli = Cli {
272+ theme : None ,
238273 git_args : args. clone ( ) ,
239274 } ;
240275 let result = cli. git_diff_args ( ) ;
@@ -248,6 +283,7 @@ mod tests {
248283 #[ test]
249284 fn test_unicode_path ( ) {
250285 let cli = Cli {
286+ theme : None ,
251287 git_args : vec ! [
252288 "HEAD" . to_string( ) ,
253289 "--" . to_string( ) ,
@@ -261,6 +297,7 @@ mod tests {
261297 #[ test]
262298 fn test_path_with_spaces ( ) {
263299 let cli = Cli {
300+ theme : None ,
264301 git_args : vec ! [
265302 "--" . to_string( ) ,
266303 "path with spaces/file.rs" . to_string( ) ,
@@ -273,6 +310,7 @@ mod tests {
273310 #[ test]
274311 fn test_at_upstream_syntax ( ) {
275312 let cli = Cli {
313+ theme : None ,
276314 git_args : vec ! [ "@{upstream}" . to_string( ) ] ,
277315 } ;
278316 assert_eq ! ( cli. git_diff_args( ) , vec![ "diff" , "-M" , "@{upstream}" ] ) ;
@@ -281,6 +319,7 @@ mod tests {
281319 #[ test]
282320 fn test_stash_ref ( ) {
283321 let cli = Cli {
322+ theme : None ,
284323 git_args : vec ! [ "stash@{0}" . to_string( ) ] ,
285324 } ;
286325 assert_eq ! ( cli. git_diff_args( ) , vec![ "diff" , "-M" , "stash@{0}" ] ) ;
@@ -289,6 +328,7 @@ mod tests {
289328 #[ test]
290329 fn test_remote_tracking_branch ( ) {
291330 let cli = Cli {
331+ theme : None ,
292332 git_args : vec ! [
293333 "origin/main" . to_string( ) ,
294334 "origin/feature/my-branch" . to_string( ) ,
@@ -303,6 +343,7 @@ mod tests {
303343 #[ test]
304344 fn test_tag_ref ( ) {
305345 let cli = Cli {
346+ theme : None ,
306347 git_args : vec ! [ "v1.0.0" . to_string( ) , "v2.0.0" . to_string( ) ] ,
307348 } ;
308349 assert_eq ! (
@@ -314,6 +355,7 @@ mod tests {
314355 #[ test]
315356 fn test_diff_filter_flag_passthrough ( ) {
316357 let cli = Cli {
358+ theme : None ,
317359 git_args : vec ! [ "--diff-filter=ACMR" . to_string( ) , "HEAD" . to_string( ) ] ,
318360 } ;
319361 assert_eq ! (
@@ -325,6 +367,7 @@ mod tests {
325367 #[ test]
326368 fn test_stat_flag_passthrough ( ) {
327369 let cli = Cli {
370+ theme : None ,
328371 git_args : vec ! [ "--stat" . to_string( ) , "HEAD" . to_string( ) ] ,
329372 } ;
330373 assert_eq ! (
@@ -336,6 +379,7 @@ mod tests {
336379 #[ test]
337380 fn test_name_only_flag_passthrough ( ) {
338381 let cli = Cli {
382+ theme : None ,
339383 git_args : vec ! [ "--name-only" . to_string( ) ] ,
340384 } ;
341385 assert_eq ! (
@@ -347,6 +391,7 @@ mod tests {
347391 #[ test]
348392 fn test_combined_flags_and_ranges ( ) {
349393 let cli = Cli {
394+ theme : None ,
350395 git_args : vec ! [
351396 "--staged" . to_string( ) ,
352397 "--diff-filter=M" . to_string( ) ,
@@ -364,6 +409,7 @@ mod tests {
364409 #[ test]
365410 fn test_empty_string_arg ( ) {
366411 let cli = Cli {
412+ theme : None ,
367413 git_args : vec ! [ "" . to_string( ) ] ,
368414 } ;
369415 let result = cli. git_diff_args ( ) ;
@@ -373,6 +419,7 @@ mod tests {
373419 #[ test]
374420 fn test_double_dash_only ( ) {
375421 let cli = Cli {
422+ theme : None ,
376423 git_args : vec ! [ "--" . to_string( ) ] ,
377424 } ;
378425 assert_eq ! ( cli. git_diff_args( ) , vec![ "diff" , "-M" , "--" ] ) ;
0 commit comments