thag_rs 0.2.0

A versatile cross-platform playground and REPL for Rust snippets, expressions and programs. Accepts a script file or dynamic options.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
/*[toml]
[dependencies]
thag_styling = { version = "0.2, thag-auto", features = ["inquire_theming"] }
*/

/// Mintty theme installer for Git Bash on Windows
///
/// This Windows-only tool installs thag themes into Mintty by copying theme files
/// to the Mintty themes directory and optionally updating the ~/.minttyrc configuration.
/// Supports selecting individual themes or entire directories of themes.
//# Purpose: Install thag themes for Mintty (Git Bash)
//# Categories: color, styling, terminal, theming, tools, windows
#[cfg(target_os = "windows")]
use inquire::set_global_render_config;

#[cfg(target_os = "windows")]
use std::{
    fs,
    path::{Path, PathBuf},
};

#[cfg(target_os = "windows")]
use thag_styling::{file_navigator, themed_inquire_config, Styleable};

use thag_styling::{auto_help, help_system::check_help_and_exit};

#[cfg(target_os = "windows")]
file_navigator! {}

#[allow(clippy::too_many_lines)]
fn main() {
    // Check for help first - automatically extracts from source comments
    let help = auto_help!();
    check_help_and_exit(&help);

    #[cfg(not(target_os = "windows"))]
    {
        println!("❌ This tool is only available on Windows systems.");
        println!("   Mintty (Git Bash) is primarily used on Windows.");
    }

    #[cfg(target_os = "windows")]
    {
        set_global_render_config(themed_inquire_config());

        println!(
            "🐙 {} - Mintty Theme Installer for Git Bash",
            "thag_mintty_add_theme".info()
        );
        println!("{}", "".repeat(70));
        println!();

        // Initialize file navigator
        let mut navigator = FileNavigator::new();

        // Get Mintty configuration paths
        let mintty_config = get_mintty_config_info()?;

        println!("📁 Mintty configuration:");
        println!(
            "   Themes directory: {}",
            mintty_config.themes_dir.display().to_string().hint()
        );
        println!(
            "   Config file: {}",
            mintty_config.config_file.display().to_string().hint()
        );

        // Check if themes directory exists
        if !mintty_config.themes_dir.exists() {
            println!("❌ Mintty themes directory not found.");
            println!("   Expected: {}", mintty_config.themes_dir.display());
            println!("   Please ensure Git for Windows is installed.");
            return Ok(());
        }

        println!("   Themes directory: {}", "Found".success());

        // Check write permissions upfront
        match check_directory_write_permission(&mintty_config.themes_dir) {
            Ok(true) => {
                println!("   Write permission: {}", "OK".success());
            }
            Ok(false) => {
                println!("❌ No write permission to themes directory.");
                println!("   Directory: {}", mintty_config.themes_dir.display());
                println!(
                    "   Please run this tool as Administrator or change directory permissions."
                );
                return Ok(());
            }
            Err(e) => {
                println!("⚠️  Could not check write permission: {}", e);
                println!("   Proceeding anyway - you may encounter permission errors.");
            }
        }

        // Check config file
        let config_exists = mintty_config.config_file.exists();
        println!(
            "   Config file: {}",
            if config_exists {
                "Found".success()
            } else {
                "Will be created".warning()
            }
        );
        println!();

        // Select themes to install
        let theme_files = select_themes(&mut navigator)?;

        if theme_files.is_empty() {
            println!("❌ No theme files selected for installation.");
            return Ok(());
        }

        // Process and install each theme
        let mut installed_themes = Vec::new();
        for theme_file in theme_files {
            match process_theme_file(&theme_file, &mintty_config.themes_dir) {
                Ok((theme_name, mintty_filename)) => {
                    println!(
                        "✅ Installed: {}{}",
                        theme_name.success(),
                        mintty_filename.info()
                    );
                    installed_themes.push((theme_name, mintty_filename));
                }
                Err(e) => {
                    println!(
                        "❌ Failed to install {}: {}",
                        theme_file.display().to_string().error(),
                        e
                    );
                }
            }
        }

        if !installed_themes.is_empty() {
            // Ask about updating config file
            let should_update_config = ask_update_config(&installed_themes)?;

            if should_update_config {
                match update_mintty_config(&mintty_config.config_file, &installed_themes) {
                    Ok(theme_name) => {
                        println!("✅ Updated ~/.minttyrc with theme: {}", theme_name.info());
                    }
                    Err(e) => {
                        println!("⚠️  Failed to update ~/.minttyrc: {}", e);
                        println!("   You can manually add themes to your config.");
                    }
                }
            }

            show_installation_summary(&installed_themes);
            show_usage_instructions();
        }
    }
}

/// Check if we have write permission to a directory
#[cfg(target_os = "windows")]
fn check_directory_write_permission(dir: &Path) -> Result<bool, Box<dyn Error>> {
    // Try to create a temporary file in the directory to test write permission
    let temp_filename = format!("thag_test_write_{}.tmp", std::process::id());
    let temp_path = dir.join(&temp_filename);

    match fs::write(&temp_path, "test") {
        Ok(()) => {
            // Clean up the test file
            let _ = fs::remove_file(&temp_path);
            Ok(true)
        }
        Err(ref e) if e.kind() == std::io::ErrorKind::PermissionDenied => Ok(false),
        Err(e) => Err(Box::new(e)),
    }
}

#[cfg(target_os = "windows")]
struct MinttyConfig {
    themes_dir: PathBuf,
    config_file: PathBuf,
}

#[cfg(target_os = "windows")]
fn get_mintty_config_info() -> Result<MinttyConfig, Box<dyn Error>> {
    // Standard Git for Windows installation path
    let themes_dir = PathBuf::from(r"C:\Program Files\Git\usr\share\mintty\themes");

    // User's home directory for config file
    let config_file = if let Some(home_dir) = dirs::home_dir() {
        home_dir.join(".minttyrc")
    } else {
        return Err("Could not determine home directory".into());
    };

    Ok(MinttyConfig {
        themes_dir,
        config_file,
    })
}

/// Select themes to install using file navigator
#[cfg(target_os = "windows")]
fn select_themes(navigator: &mut FileNavigator) -> Result<Vec<PathBuf>, Box<dyn Error>> {
    use inquire::{Confirm, MultiSelect, Select};

    let selection_options = vec![
        "Select mintty theme files individually",
        "Select mintty theme files in bulk from directory",
    ];

    let mut selected_themes = Vec::new();

    // Try to navigate to exported themes directory if it exists
    let _ = navigator.navigate_to_path("exported_themes/mintty");
    if !navigator.current_path().join("mintty").exists() {
        let _ = navigator.navigate_to_path("exported_themes");
    }

    let selection_method =
        Select::new("How would you like to select themes?", selection_options).prompt()?;

    match selection_method {
        "Select mintty theme files individually" => {
            loop {
                println!("\n📁 Select a mintty theme file (no extension):");
                if let Ok(theme_file) = select_file(navigator, None, false) {
                    // Check that it's a mintty theme file (no extension)
                    if theme_file.extension().is_none() {
                        selected_themes.push(theme_file);
                    } else {
                        println!("⚠️  Mintty theme files should have no extension. Skipping.");
                        continue;
                    }

                    let add_more = Confirm::new("Add another theme file?")
                        .with_default(false)
                        .prompt()?;

                    if !add_more {
                        break;
                    }
                } else if selected_themes.is_empty() {
                    return Ok(vec![]);
                } else {
                    break;
                }
            }

            Ok(selected_themes)
        }
        "Select mintty theme files in bulk from directory" => {
            println!("\n📁 Select directory containing mintty theme files:");
            match select_directory(navigator, true) {
                Ok(theme_dir) => {
                    let theme_files = find_theme_files_in_directory(&theme_dir)?;

                    if theme_files.is_empty() {
                        println!("❌ No mintty theme files found in directory");
                        return Ok(vec![]);
                    }

                    for theme_file in theme_files {
                        selected_themes.push(theme_file);
                    }

                    // Let user confirm which themes to install
                    if selected_themes.len() > 1 {
                        let confirmed_themes = MultiSelect::new(
                            "Confirm themes to install:",
                            selected_themes
                                .iter()
                                .map(|v| v.display().to_string())
                                .collect::<Vec<_>>(),
                        )
                        .with_default(&(0..selected_themes.len()).collect::<Vec<_>>())
                        .prompt()?;

                        Ok(confirmed_themes
                            .iter()
                            .map(PathBuf::from)
                            .collect::<Vec<_>>())
                    } else {
                        Ok(selected_themes)
                    }
                }
                Err(_) => Ok(vec![]),
            }
        }
        _ => Ok(vec![]),
    }
}

/// Find mintty theme files in a directory (files with no extension)
#[cfg(target_os = "windows")]
fn find_theme_files_in_directory(dir: &Path) -> Result<Vec<PathBuf>, Box<dyn Error>> {
    let mut theme_files = Vec::new();

    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();

        if path.is_file() && path.extension().is_none() {
            // Additional check: ensure it looks like a mintty theme file
            if is_mintty_theme_file(&path)? {
                theme_files.push(path);
            }
        }
    }

    theme_files.sort();
    Ok(theme_files)
}

/// Check if a file appears to be a mintty theme file
#[cfg(target_os = "windows")]
fn is_mintty_theme_file(path: &Path) -> Result<bool, Box<dyn Error>> {
    if let Ok(content) = fs::read_to_string(path) {
        // Check for mintty-specific configuration keys
        Ok(content.contains("BackgroundColour=") || content.contains("ForegroundColour="))
    } else {
        Ok(false)
    }
}

/// Copy a mintty theme file to the themes directory
#[cfg(target_os = "windows")]
fn process_theme_file(
    theme_file: &Path,
    themes_dir: &Path,
) -> Result<(String, String), Box<dyn Error>> {
    let theme_filename = theme_file
        .file_name()
        .and_then(|name| name.to_str())
        .ok_or("Invalid theme filename")?
        .to_string();

    let destination_path = themes_dir.join(&theme_filename);

    // Copy the theme file to the mintty themes directory
    fs::copy(theme_file, &destination_path)
        .map_err(|e| format!("Failed to copy theme file: {}", e))?;

    // Extract theme name from filename (remove thag- prefix if present)
    let theme_name = if theme_filename.starts_with("thag-") {
        theme_filename
            .strip_prefix("thag-")
            .unwrap_or(&theme_filename)
    } else {
        &theme_filename
    };

    let display_name = theme_name.replace('-', " ").replace('_', " ");

    Ok((display_name, theme_filename))
}

#[cfg(target_os = "windows")]
fn ask_update_config(installed_themes: &[(String, String)]) -> Result<bool, Box<dyn Error>> {
    use inquire::Confirm;

    if installed_themes.len() == 1 {
        let confirm = Confirm::new(&format!(
            "Would you like to set '{}' as the active
 theme in ~/.minttyrc?",
            installed_themes[0].0
        ))
        .with_default(true)
        .prompt()?;

        Ok(confirm)
    } else {
        let confirm = Confirm::new(
            "Would you like to set one of the installed themes as active in ~/.minttyrc?",
        )
        .with_default(true)
        .prompt()?;

        Ok(confirm)
    }
}

#[cfg(target_os = "windows")]
fn update_mintty_config(
    config_file: &Path,
    installed_themes: &[(String, String)],
) -> Result<String, Box<dyn Error>> {
    use inquire::Select;

    let theme_to_set = if installed_themes.len() == 1 {
        &installed_themes[0].1
    } else {
        // Let user choose which theme to activate
        let theme_names: Vec<String> = installed_themes
            .iter()
            .map(|(name, _)| name.clone())
            .collect();

        let selected_name = Select::new("Select which theme to activate:", theme_names).prompt()?;

        // Find the corresponding filename
        installed_themes
            .iter()
            .find(|(name, _)| name == &selected_name)
            .map(|(_, filename)| filename)
            .ok_or("Selected theme not found")?
    };

    // Read existing config or create new one
    let mut config_content = if config_file.exists() {
        fs::read_to_string(config_file)?
    } else {
        String::new()
    };

    // Remove existing ThemeFile lines
    config_content = config_content
        .lines()
        .filter(|line| !line.starts_with("ThemeFile="))
        .collect::<Vec<_>>()
        .join("\n");

    // Add new theme file line
    if !config_content.is_empty() && !config_content.ends_with('\n') {
        config_content.push('\n');
    }
    config_content.push_str(&format!("ThemeFile={}\n", theme_to_set));

    // Write updated config
    fs::write(config_file, config_content)?;

    Ok(theme_to_set.clone())
}

#[cfg(target_os = "windows")]
fn show_installation_summary(installed_themes: &[(String, String)]) {
    println!();
    println!("🎉 Installation Summary:");
    println!("{}", "".repeat(50));

    for (theme_name, filename) in installed_themes {
        println!("{}{}", theme_name.success(), filename.info());
    }

    println!();
    println!(
        "📁 Themes installed to: {}",
        r"C:\Program Files\Git\usr\share\mintty\themes\".info()
    );
}

#[cfg(target_os = "windows")]
fn show_usage_instructions() {
    println!("🔧 How to use your new themes:");
    println!("{}", "".repeat(40));
    println!("1. Ensure your `thag_styling` theme is set to match.");
    println!("   E.g. `export THAG_THEME=<corresponding thag_styling theme>` in `~/.bashrc` or `~/.zshrc`");
    println!("   or as preferred light/dark theme via `thag -C` (ensure background color of `thag_styling` theme matches that of terminal)");
    println!("2. Open Git Bash (Mintty)");
    println!("3. Right-click on the title bar and select 'Options...'");
    println!("4. Go to the 'Looks' tab");
    println!("5. Select your theme from the 'Theme' dropdown");
    println!("6. Click 'Apply' or 'OK'");
    println!();
    println!("💡 Tip: The theme will apply to all new Mintty windows.");
    println!("   Existing windows may need to be restarted to see the changes.");
}

#[cfg(test)]
mod tests {
    #[cfg(target_os = "windows")]
    use super::*;

    #[cfg(target_os = "windows")]
    #[test]
    fn test_mintty_config_info() {
        // Test that we can get mintty configuration info
        if let Ok(config) = get_mintty_config_info() {
            assert!(config.themes_dir.to_string_lossy().contains("mintty"));
            assert!(config.config_file.to_string_lossy().contains("minttyrc"));
        }
    }

    #[cfg(target_os = "windows")]
    #[test]
    fn test_mintty_theme_file_detection() {
        // Test helper for file detection - this would need actual files to test properly
        assert!(true);
    }

    #[cfg(not(target_os = "windows"))]
    #[test]
    fn test_non_windows_placeholder() {
        // This test exists to ensure the test module compiles on non-Windows platforms
        // assert!(true);
    }
}