File size: 30,281 Bytes
d90101d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
use std::fs;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::process::Stdio;

use anyhow::{Context, Result};
use clap::CommandFactory;
use clap_complete::generate;
use clap_complete::shells::Zsh;
use include_dir::{Dir, include_dir};

use crate::cli::Cli;

/// Embeds shell plugin files for zsh integration
static ZSH_PLUGIN_LIB: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/../../shell-plugin/lib");

/// Generates the complete zsh plugin by combining embedded files and clap
/// completions
pub fn generate_zsh_plugin() -> Result<String> {
    let mut output = String::new();

    // Iterate through all embedded files in shell-plugin/lib, stripping comments
    // and empty lines. All files in this directory are .zsh files.
    for file in forge_embed::files(&ZSH_PLUGIN_LIB) {
        let content = super::normalize_script(std::str::from_utf8(file.contents())?);
        for line in content.lines() {
            let trimmed = line.trim();
            // Skip empty lines and comment lines
            if !trimmed.is_empty() && !trimmed.starts_with('#') {
                output.push_str(line);
                output.push('\n');
            }
        }
    }

    // Generate clap completions for the CLI
    let mut cmd = Cli::command();
    let mut completions = Vec::new();
    generate(Zsh, &mut cmd, "forge", &mut completions);

    // Append completions to the output with clear separator
    let completions_str = String::from_utf8(completions)?;
    output.push_str("\n# --- Clap Completions ---\n");
    output.push_str(&completions_str);

    // Set environment variable to indicate plugin is loaded (with timestamp)
    output.push_str("\n_FORGE_PLUGIN_LOADED=$(date +%s)\n");

    Ok(output)
}

/// Generates the ZSH theme for Forge
pub fn generate_zsh_theme() -> Result<String> {
    let mut content =
        super::normalize_script(include_str!("../../../../shell-plugin/forge.theme.zsh"));

    // Set environment variable to indicate theme is loaded (with timestamp)
    content.push_str("\n_FORGE_THEME_LOADED=$(date +%s)\n");

    Ok(content)
}

/// Creates a temporary zsh script file for Windows execution
fn create_temp_zsh_script(script_content: &str) -> Result<(tempfile::TempDir, PathBuf)> {
    use std::io::Write;

    let temp_dir = tempfile::tempdir().context("Failed to create temp directory")?;
    let script_path = temp_dir.path().join("forge_script.zsh");
    let mut file = fs::File::create(&script_path).context("Failed to create temp script file")?;
    file.write_all(script_content.as_bytes())
        .context("Failed to write temp script")?;

    Ok((temp_dir, script_path))
}

/// Executes a ZSH script with streaming output
///
/// # Arguments
///
/// * `script_content` - The ZSH script content to execute
/// * `script_name` - Descriptive name for the script (used in error messages)
///
/// # Errors
///
/// Returns error if the script cannot be executed, if output streaming fails,
/// or if the script exits with a non-zero status code
fn execute_zsh_script_with_streaming(script_content: &str, script_name: &str) -> Result<()> {
    let script_content = super::normalize_script(script_content);

    // On Unix, pass the script via `zsh -c`. Command::arg() uses execve,
    // which forwards arguments directly without shell interpretation, so
    // embedded quotes are safe.
    //
    // On Windows, we write the script to a temp file and run `zsh -f <file>`
    // instead. A temp file is necessary because:
    //   1. CI has core.autocrlf=true, so checked-out files contain CRLF; writing
    //      through normalize_script ensures the temp file has LF.
    //   2. CreateProcess mangles quotes, so passing the script via -c corrupts any
    //      embedded quoting.
    //   3. Piping via stdin is unreliable -- Windows caps pipe buffer size, which
    //      can truncate or block on larger scripts.
    // The -f flag also prevents ~/.zshrc from loading during execution.
    let (_temp_dir, mut child) = if cfg!(windows) {
        let (temp_dir, script_path) = create_temp_zsh_script(&script_content)?;
        let child = std::process::Command::new("zsh")
            // -f: don't load ~/.zshrc (prevents theme loading during doctor)
            .arg("-f")
            .arg(script_path.to_string_lossy().as_ref())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .context(format!("Failed to execute zsh {} script", script_name))?;
        // Keep temp_dir alive by boxing it in the tuple
        (Some(temp_dir), child)
    } else {
        let child = std::process::Command::new("zsh")
            .arg("-c")
            .arg(&script_content)
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .context(format!("Failed to execute zsh {} script", script_name))?;
        (None, child)
    };

    // Get stdout and stderr handles
    let stdout = child.stdout.take().context("Failed to capture stdout")?;
    let stderr = child.stderr.take().context("Failed to capture stderr")?;

    // Use scoped threads for safer streaming with automatic joining
    std::thread::scope(|s| {
        // Stream stdout line by line
        s.spawn(|| {
            let stdout_reader = BufReader::new(stdout);
            for line in stdout_reader.lines() {
                match line {
                    Ok(line) => println!("{}", line),
                    Err(e) => eprintln!("Error reading stdout: {}", e),
                }
            }
        });

        // Stream stderr line by line
        s.spawn(|| {
            let stderr_reader = BufReader::new(stderr);
            for line in stderr_reader.lines() {
                match line {
                    Ok(line) => eprintln!("{}", line),
                    Err(e) => eprintln!("Error reading stderr: {}", e),
                }
            }
        });
    });

    // Wait for the child process to complete
    let status = child
        .wait()
        .context(format!("Failed to wait for zsh {} script", script_name))?;

    if !status.success() {
        let exit_code = status
            .code()
            .map_or_else(|| "unknown".to_string(), |code| code.to_string());

        anyhow::bail!(
            "ZSH {} script failed with exit code: {}",
            script_name,
            exit_code
        );
    }

    Ok(())
}

/// Runs diagnostics on the ZSH shell environment with streaming output
///
/// # Errors
///
/// Returns error if the doctor script cannot be executed
pub fn run_zsh_doctor() -> Result<()> {
    let script_content = include_str!("../../../../shell-plugin/doctor.zsh");
    execute_zsh_script_with_streaming(script_content, "doctor")
}

/// Shows ZSH keyboard shortcuts with streaming output
///
/// # Errors
///
/// Returns error if the keyboard script cannot be executed
pub fn run_zsh_keyboard() -> Result<()> {
    let script_content = include_str!("../../../../shell-plugin/keyboard.zsh");
    execute_zsh_script_with_streaming(script_content, "keyboard")
}

/// Represents the state of markers in a file
enum MarkerState {
    /// No markers found
    NotFound,
    /// Valid markers with correct positions
    Valid { start: usize, end: usize },
    /// Invalid markers (incorrect order or incomplete)
    Invalid {
        start: Option<usize>,
        end: Option<usize>,
    },
}

/// Parses the file content to find and validate marker positions
///
/// # Arguments
///
/// * `lines` - The lines of the file to parse
/// * `start_marker` - The start marker to look for
/// * `end_marker` - The end marker to look for
fn parse_markers(lines: &[String], start_marker: &str, end_marker: &str) -> MarkerState {
    let start_idx = lines.iter().position(|line| line.trim() == start_marker);
    let end_idx = lines.iter().position(|line| line.trim() == end_marker);

    match (start_idx, end_idx) {
        (Some(start), Some(end)) if start < end => MarkerState::Valid { start, end },
        (None, None) => MarkerState::NotFound,
        (start, end) => MarkerState::Invalid { start, end },
    }
}

/// Result of ZSH setup operation
#[derive(Debug)]
pub struct ZshSetupResult {
    /// Status message describing what was done
    pub message: String,
    /// Path to backup file if one was created
    pub backup_path: Option<PathBuf>,
}

/// Sets up ZSH integration with optional nerd font and editor configuration
///
/// # Arguments
///
/// * `disable_nerd_font` - If true, adds NERD_FONT=0 to .zshrc
/// * `forge_editor` - If Some(editor), adds FORGE_EDITOR export to .zshrc
///
/// # Errors
///
/// Returns error if:
/// - The HOME environment variable is not set
/// - The .zshrc file cannot be read or written
/// - Invalid forge markers are found (incomplete or incorrectly ordered)
/// - A backup of the existing .zshrc cannot be created
pub fn setup_zsh_integration(
    disable_nerd_font: bool,
    forge_editor: Option<&str>,
) -> Result<ZshSetupResult> {
    const START_MARKER: &str = "# >>> forge initialize >>>";
    const END_MARKER: &str = "# <<< forge initialize <<<";
    const FORGE_INIT_CONFIG_RAW: &str = include_str!("../../../../shell-plugin/forge.setup.zsh");
    let forge_init_config = super::normalize_script(FORGE_INIT_CONFIG_RAW);

    let home = std::env::var("HOME").context("HOME environment variable not set")?;
    let zdotdir = std::env::var("ZDOTDIR").unwrap_or_else(|_| home.clone());
    let zshrc_path = PathBuf::from(&zdotdir).join(".zshrc");

    // Read existing .zshrc or create new one
    let content = if zshrc_path.exists() {
        fs::read_to_string(&zshrc_path)
            .context(format!("Failed to read {}", zshrc_path.display()))?
    } else {
        String::new()
    };

    let mut lines: Vec<String> = content.lines().map(String::from).collect();

    // Parse markers to determine their state
    let marker_state = parse_markers(&lines, START_MARKER, END_MARKER);

    // Build the forge config block with markers
    let mut forge_config: Vec<String> = vec![START_MARKER.to_string()];
    forge_config.extend(forge_init_config.lines().map(String::from));

    // Add nerd font configuration if requested
    if disable_nerd_font {
        forge_config.push(String::new()); // Add blank line before comment
        forge_config.push(
            "# Disable Nerd Fonts (set during setup - icons not displaying correctly)".to_string(),
        );
        forge_config.push("# To re-enable: remove this line and install a Nerd Font from https://www.nerdfonts.com/".to_string());
        forge_config.push("export NERD_FONT=0".to_string());
    }

    // Add editor configuration if requested
    if let Some(editor) = forge_editor {
        forge_config.push(String::new()); // Add blank line before comment
        forge_config.push("# Editor for editing prompts (set during setup)".to_string());
        forge_config.push("# To change: update FORGE_EDITOR or remove to use $EDITOR".to_string());
        forge_config.push(format!("export FORGE_EDITOR=\"{}\"", editor));
    }

    forge_config.push(END_MARKER.to_string());

    // Add or update forge configuration block based on marker state
    let (new_content, config_action) = match marker_state {
        MarkerState::Valid { start, end } => {
            // Markers exist - replace content between them
            lines.splice(start..=end, forge_config.iter().cloned());
            (lines.join("\n") + "\n", "updated")
        }
        MarkerState::Invalid { start, end } => {
            let location = match (start, end) {
                (Some(s), Some(e)) => Some(format!("{}:{}-{}", zshrc_path.display(), s + 1, e + 1)),
                (Some(s), None) => Some(format!("{}:{}", zshrc_path.display(), s + 1)),
                (None, Some(e)) => Some(format!("{}:{}", zshrc_path.display(), e + 1)),
                (None, None) => None,
            };

            let mut error =
                anyhow::anyhow!("Invalid forge markers found in {}", zshrc_path.display());
            if let Some(loc) = location {
                error = error.context(format!("Markers found at {}", loc));
            }
            return Err(error);
        }
        MarkerState::NotFound => {
            // No markers - add them at the end
            // Add blank line before markers if file is not empty and doesn't end with blank
            // line
            if lines.last().is_some_and(|l| !l.trim().is_empty()) {
                lines.push(String::new());
            }

            lines.extend(forge_config.iter().cloned());
            (lines.join("\n") + "\n", "added")
        }
    };

    // Create backup of existing .zshrc if it exists
    let backup_path = if zshrc_path.exists() {
        // Generate timestamp for backup filename
        let timestamp = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S");

        // Safe to unwrap: zshrc_path was constructed from a valid HOME/ZDOTDIR path
        let parent = zshrc_path
            .parent()
            .context("zshrc path has no parent directory")?;
        let filename = zshrc_path
            .file_name()
            .context("zshrc path has no filename")?;
        let filename_str = filename
            .to_str()
            .context("zshrc filename is not valid UTF-8")?;

        let backup = parent.join(format!("{}.bak.{}", filename_str, timestamp));
        fs::copy(&zshrc_path, &backup)
            .context(format!("Failed to create backup at {}", backup.display()))?;
        Some(backup)
    } else {
        None
    };

    // Write back to .zshrc
    fs::write(&zshrc_path, &new_content)
        .context(format!("Failed to write to {}", zshrc_path.display()))?;

    Ok(ZshSetupResult {
        message: format!("forge plugins {}", config_action),
        backup_path,
    })
}

#[cfg(test)]
mod tests {
    use std::sync::{LazyLock, Mutex};

    use super::*;

    // Mutex to ensure tests that modify environment variables run serially
    static ENV_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));

    /// Test that the doctor script executes and streams output
    /// Note: The script may fail with non-zero exit code in test environment
    /// (e.g., plugin not loaded), or zsh may not be available in CI
    #[test]
    fn test_run_zsh_doctor_streaming() {
        // SAFETY: No mutex needed for single test - setting env var for test isolation
        unsafe {
            std::env::set_var("FORGE_SKIP_INTERACTIVE", "1");
        }

        let actual = run_zsh_doctor();

        // Clean up
        // SAFETY: No mutex needed for single test - removing env var after test
        unsafe {
            std::env::remove_var("FORGE_SKIP_INTERACTIVE");
        }

        // The doctor script runs successfully even if it reports failures
        // (failures are expected in test environment where plugin isn't loaded)
        // Also accept cases where zsh is not available in CI environment
        match actual {
            Ok(_) => {
                // Success case
            }
            Err(e) => {
                // Check if it's a non-zero exit code error or zsh not available (both expected
                // in tests)
                let error_msg = e.to_string();
                assert!(
                    error_msg.contains("exit code") || error_msg.contains("Failed to execute"),
                    "Unexpected error: {}",
                    error_msg
                );
            }
        }
    }

    #[test]
    fn test_generated_plugin_wraps_zle_commands_with_osc133_markers() {
        use pretty_assertions::assert_eq;

        let fixture = generate_zsh_plugin().unwrap();

        // Verify OSC 133 B and C markers are emitted before the action dispatch
        let actual = fixture.contains("    _forge_osc133_emit \"B\"\n    _forge_osc133_emit \"C\"")
            // The buffer is redisplayed with cursor at end so user input stays visible
            && fixture.contains("CURSOR=${#BUFFER}\n    zle redisplay")
            // The case dispatch follows the buffer redisplay
            && fixture.contains("    case \"$user_action\" in")
            // _forge_reset pads with newlines before clearing to prevent ZLE from clearing
            // conversation output lines (see helpers.zsh:139-157)
            && fixture.contains("for ((_i=1; _i<pad; _i++)); do print; done\n")
            && fixture.contains("BUFFER=\"\"\n  CURSOR=0")
            // D and A markers follow the action, with _forge_reset at the end
            && fixture.contains(
                "    local action_status=$?\n    _forge_osc133_emit \"D;$action_status\"\n    _forge_osc133_emit \"A\"\n    _forge_reset",
            );
        let expected = true;
        assert_eq!(actual, expected);
    }

    /// Regression: forge keybindings must survive zsh-vi-mode's `zvm_init`
    /// by re-applying via `zvm_after_init_commands` (#2681).
    #[test]
    fn test_generated_plugin_registers_zvm_after_init_hook() {
        use pretty_assertions::assert_eq;

        let fixture = generate_zsh_plugin().unwrap();
        let actual = fixture.contains("function _forge_apply_keybindings()")
            && fixture.contains("typeset -ga zvm_after_init_commands")
            && fixture.contains("zvm_after_init_commands+=('_forge_apply_keybindings')");
        let expected = true;
        assert_eq!(actual, expected);
    }

    #[test]
    fn test_setup_zsh_integration_without_nerd_font_config() {
        use tempfile::TempDir;

        // Lock to prevent parallel test execution that modifies env vars
        let _guard = ENV_LOCK.lock().unwrap();

        // Create a temporary directory for the test
        let temp_dir = TempDir::new().unwrap();
        let zshrc_path = temp_dir.path().join(".zshrc");

        // Set HOME to temp directory
        let original_home = std::env::var("HOME").ok();
        let original_zdotdir = std::env::var("ZDOTDIR").ok();

        // SAFETY: We hold ENV_LOCK to prevent concurrent environment modifications
        unsafe {
            std::env::set_var("HOME", temp_dir.path());
            std::env::remove_var("ZDOTDIR");
        }

        // Run setup without nerd font config
        let actual = setup_zsh_integration(false, None);

        // Restore environment first
        // SAFETY: We hold ENV_LOCK to prevent concurrent environment modifications
        unsafe {
            if let Some(home) = original_home {
                std::env::set_var("HOME", home);
            } else {
                std::env::remove_var("HOME");
            }
            if let Some(zdotdir) = original_zdotdir {
                std::env::set_var("ZDOTDIR", zdotdir);
            } else {
                std::env::remove_var("ZDOTDIR");
            }
        }

        assert!(actual.is_ok(), "Setup should succeed: {:?}", actual);

        // Read the generated .zshrc
        assert!(
            zshrc_path.exists(),
            "zshrc file should be created at {:?}",
            zshrc_path
        );
        let content = fs::read_to_string(&zshrc_path).expect("Should be able to read zshrc");

        // Should not contain NERD_FONT=0
        assert!(!content.contains("NERD_FONT=0"));

        // Should contain the markers
        assert!(content.contains("# >>> forge initialize >>>"));
        assert!(content.contains("# <<< forge initialize <<<"));
    }

    #[test]
    fn test_setup_zsh_integration_with_nerd_font_disabled() {
        use tempfile::TempDir;

        // Lock to prevent parallel test execution that modifies env vars
        let _guard = ENV_LOCK.lock().unwrap();

        // Create a temporary directory for the test
        let temp_dir = TempDir::new().unwrap();
        let zshrc_path = temp_dir.path().join(".zshrc");

        // Set HOME to temp directory
        let original_home = std::env::var("HOME").ok();
        let original_zdotdir = std::env::var("ZDOTDIR").ok();

        // SAFETY: We hold ENV_LOCK to prevent concurrent environment modifications
        unsafe {
            std::env::set_var("HOME", temp_dir.path());
            std::env::set_var("ZDOTDIR", temp_dir.path());
        }

        // Run setup with nerd font disabled
        let actual = setup_zsh_integration(true, None);
        assert!(actual.is_ok(), "Setup should succeed: {:?}", actual);

        // Read the generated .zshrc
        assert!(zshrc_path.exists(), "zshrc file should be created");
        let content = fs::read_to_string(&zshrc_path).expect("Should be able to read zshrc");

        // Should contain NERD_FONT=0 with explanatory comments
        assert!(
            content.contains("export NERD_FONT=0"),
            "Content should contain NERD_FONT=0:\n{}",
            content
        );
        assert!(
            content.contains(
                "# Disable Nerd Fonts (set during setup - icons not displaying correctly)"
            ),
            "Should contain explanation comment"
        );
        assert!(content.contains("# To re-enable: remove this line and install a Nerd Font from https://www.nerdfonts.com/"), "Should contain re-enable instructions");

        // Should contain the markers
        assert!(content.contains("# >>> forge initialize >>>"));
        assert!(content.contains("# <<< forge initialize <<<"));

        // Restore environment
        // SAFETY: We hold ENV_LOCK to prevent concurrent environment modifications
        unsafe {
            if let Some(home) = original_home {
                std::env::set_var("HOME", home);
            }
            if let Some(zdotdir) = original_zdotdir {
                std::env::set_var("ZDOTDIR", zdotdir);
            }
        }
    }

    #[test]
    fn test_setup_zsh_integration_with_editor() {
        use tempfile::TempDir;

        // Lock to prevent parallel test execution that modifies env vars
        let _guard = ENV_LOCK.lock().unwrap();

        // Create a temporary directory for the test
        let temp_dir = TempDir::new().unwrap();
        let zshrc_path = temp_dir.path().join(".zshrc");

        // Set HOME to temp directory
        let original_home = std::env::var("HOME").ok();
        let original_zdotdir = std::env::var("ZDOTDIR").ok();

        // SAFETY: We hold ENV_LOCK to prevent concurrent environment modifications
        unsafe {
            std::env::set_var("HOME", temp_dir.path());
            std::env::remove_var("ZDOTDIR");
        }

        // Run setup with editor configuration
        let actual = setup_zsh_integration(false, Some("code --wait"));

        assert!(actual.is_ok(), "Setup should succeed: {:?}", actual);

        // Read the generated .zshrc
        assert!(zshrc_path.exists(), "zshrc file should be created");
        let content = fs::read_to_string(&zshrc_path).expect("Should be able to read zshrc");

        // Should contain FORGE_EDITOR with explanatory comments
        assert!(
            content.contains("export FORGE_EDITOR=\"code --wait\""),
            "Content should contain FORGE_EDITOR:\n{}",
            content
        );
        assert!(
            content.contains("# Editor for editing prompts (set during setup)"),
            "Should contain editor explanation comment"
        );
        assert!(
            content.contains("# To change: update FORGE_EDITOR or remove to use $EDITOR"),
            "Should contain editor change instructions"
        );

        // Should contain the markers
        assert!(content.contains("# >>> forge initialize >>>"));
        assert!(content.contains("# <<< forge initialize <<<"));

        // Restore environment
        // SAFETY: We hold ENV_LOCK to prevent concurrent environment modifications
        unsafe {
            if let Some(home) = original_home {
                std::env::set_var("HOME", home);
            } else {
                std::env::remove_var("HOME");
            }
            if let Some(zdotdir) = original_zdotdir {
                std::env::set_var("ZDOTDIR", zdotdir);
            } else {
                std::env::remove_var("ZDOTDIR");
            }
        }
    }

    #[test]
    fn test_setup_zsh_integration_with_both_configs() {
        use tempfile::TempDir;

        // Lock to prevent parallel test execution that modifies env vars
        let _guard = ENV_LOCK.lock().unwrap();

        // Create a temporary directory for the test
        let temp_dir = TempDir::new().unwrap();
        let zshrc_path = temp_dir.path().join(".zshrc");

        // Set HOME to temp directory
        let original_home = std::env::var("HOME").ok();
        let original_zdotdir = std::env::var("ZDOTDIR").ok();

        // SAFETY: We hold ENV_LOCK to prevent concurrent environment modifications
        unsafe {
            std::env::set_var("HOME", temp_dir.path());
            std::env::set_var("ZDOTDIR", temp_dir.path());
        }

        // Run setup with both nerd font disabled and editor configured
        let actual = setup_zsh_integration(true, Some("vim"));
        assert!(actual.is_ok(), "Setup should succeed: {:?}", actual);

        // Read the generated .zshrc
        assert!(zshrc_path.exists(), "zshrc file should be created");
        let content = fs::read_to_string(&zshrc_path).expect("Should be able to read zshrc");

        // Should contain both configurations
        assert!(
            content.contains("export NERD_FONT=0"),
            "Content should contain NERD_FONT=0:\n{}",
            content
        );
        assert!(
            content.contains("export FORGE_EDITOR=\"vim\""),
            "Content should contain FORGE_EDITOR:\n{}",
            content
        );

        // Should contain the markers
        assert!(content.contains("# >>> forge initialize >>>"));
        assert!(content.contains("# <<< forge initialize <<<"));

        // Restore environment
        // SAFETY: We hold ENV_LOCK to prevent concurrent environment modifications
        unsafe {
            if let Some(home) = original_home {
                std::env::set_var("HOME", home);
            }
            if let Some(zdotdir) = original_zdotdir {
                std::env::set_var("ZDOTDIR", zdotdir);
            }
        }
    }

    #[test]
    fn test_setup_zsh_integration_updates_existing_markers() {
        use tempfile::TempDir;

        // Lock to prevent parallel test execution that modifies env vars
        let _guard = ENV_LOCK.lock().unwrap();

        // Create a temporary directory for the test
        let temp_dir = TempDir::new().unwrap();
        let zshrc_path = temp_dir.path().join(".zshrc");

        // Set HOME to temp directory
        let original_home = std::env::var("HOME").ok();
        let original_zdotdir = std::env::var("ZDOTDIR").ok();

        // SAFETY: We hold ENV_LOCK to prevent concurrent environment modifications
        unsafe {
            std::env::set_var("HOME", temp_dir.path());
            std::env::remove_var("ZDOTDIR");
        }

        // First setup - with nerd font disabled
        let result = setup_zsh_integration(true, None);
        assert!(result.is_ok(), "Initial setup should succeed: {:?}", result);

        // First setup should not create a backup (no existing file)
        assert!(
            result.as_ref().unwrap().backup_path.is_none(),
            "Should not create backup on initial setup"
        );

        let content = fs::read_to_string(&zshrc_path).expect("Should be able to read zshrc");
        assert!(
            content.contains("export NERD_FONT=0"),
            "Should contain NERD_FONT=0 after first setup"
        );
        assert!(
            !content.contains("export FORGE_EDITOR"),
            "Should not contain FORGE_EDITOR after first setup"
        );

        // Second setup - without nerd font but with editor
        let result = setup_zsh_integration(false, Some("nvim"));
        assert!(result.is_ok(), "Update setup should succeed: {:?}", result);

        // Second setup should create a backup (existing file)
        let backup_path = result.as_ref().unwrap().backup_path.as_ref();
        assert!(backup_path.is_some(), "Should create backup on update");
        let backup = backup_path.unwrap();
        assert!(backup.exists(), "Backup file should exist at {:?}", backup);

        // Verify backup filename contains timestamp
        let backup_name = backup.file_name().unwrap().to_str().unwrap();
        assert!(
            backup_name.starts_with(".zshrc.bak."),
            "Backup filename should start with .zshrc.bak.: {}",
            backup_name
        );
        assert!(
            backup_name.len() > ".zshrc.bak.".len(),
            "Backup filename should include timestamp: {}",
            backup_name
        );

        let content = fs::read_to_string(&zshrc_path).expect("Should be able to read zshrc");

        // Should not contain NERD_FONT=0 anymore
        assert!(
            !content.contains("export NERD_FONT=0"),
            "Should not contain NERD_FONT=0 after update:\n{}",
            content
        );

        // Should contain the editor
        assert!(
            content.contains("export FORGE_EDITOR=\"nvim\""),
            "Should contain FORGE_EDITOR after update:\n{}",
            content
        );

        // Should still have markers
        assert!(content.contains("# >>> forge initialize >>>"));
        assert!(content.contains("# <<< forge initialize <<<"));

        // Should only have one set of markers
        assert_eq!(
            content.matches("# >>> forge initialize >>>").count(),
            1,
            "Should have exactly one start marker"
        );
        assert_eq!(
            content.matches("# <<< forge initialize <<<").count(),
            1,
            "Should have exactly one end marker"
        );

        // Restore environment
        // SAFETY: We hold ENV_LOCK to prevent concurrent environment modifications
        unsafe {
            if let Some(home) = original_home {
                std::env::set_var("HOME", home);
            } else {
                std::env::remove_var("HOME");
            }
            if let Some(zdotdir) = original_zdotdir {
                std::env::set_var("ZDOTDIR", zdotdir);
            } else {
                std::env::remove_var("ZDOTDIR");
            }
        }
    }
}