Skip to main content

vtcode_core/terminal_setup/
wizard.rs

1//! Interactive terminal setup wizard.
2//!
3//! Guides users through configuring their terminal emulator for VT Code.
4
5use crate::VTCodeConfig;
6use crate::utils::ansi::{AnsiRenderer, MessageStyle};
7use crate::utils::file_utils::read_file_with_context_sync;
8use anyhow::Result;
9
10use super::backup::ConfigBackupManager;
11use super::detector::{TerminalFeature, TerminalSetupAvailability, TerminalType};
12
13/// Run the interactive terminal setup wizard
14pub async fn run_terminal_setup_wizard(renderer: &mut AnsiRenderer, _config: &VTCodeConfig) -> Result<()> {
15    // Step 1: Welcome and Detection
16    display_welcome(renderer)?;
17
18    let terminal_type = TerminalType::detect()?;
19    run_setup_for_terminal(renderer, terminal_type, || terminal_type.config_path())
20}
21
22fn run_setup_for_terminal(
23    renderer: &mut AnsiRenderer,
24    terminal_type: TerminalType,
25    resolve_config_path: impl FnOnce() -> Result<std::path::PathBuf>,
26) -> Result<()> {
27    renderer.line(MessageStyle::Status, &format!("Detected terminal: {}", terminal_type.name()))?;
28
29    // Step 2: Feature Selection (for now, show what will be configured)
30    renderer.line_if_not_empty(MessageStyle::Info)?;
31    renderer.line(MessageStyle::Info, "Features to configure:")?;
32
33    let features = vec![
34        TerminalFeature::Multiline,
35        TerminalFeature::CopyPaste,
36        TerminalFeature::ShellIntegration,
37        TerminalFeature::ThemeSync,
38        TerminalFeature::Notifications,
39    ];
40
41    for feature in &features {
42        let supported = terminal_type.supports_feature(*feature);
43        let status = if supported { "✓" } else { "✗ (not supported)" };
44        renderer.line(
45            if supported {
46                MessageStyle::Status
47            } else {
48                MessageStyle::Info
49            },
50            &format!("  {} {}", status, feature.name()),
51        )?;
52    }
53
54    match terminal_type.terminal_setup_availability() {
55        TerminalSetupAvailability::NativeSupport => {
56            render_guidance_messages(renderer, &native_terminal_setup_messages(terminal_type))?;
57            return Ok(());
58        }
59        TerminalSetupAvailability::GuidanceOnly => {
60            render_guidance_messages(renderer, &guidance_only_messages(terminal_type))?;
61            return Ok(());
62        }
63        TerminalSetupAvailability::Offered => {}
64    }
65
66    if let Some(instructions) = manual_setup_instructions(terminal_type, &features)? {
67        render_guidance_messages(renderer, &instructions.lines().map(str::to_string).collect::<Vec<_>>())?;
68        return Ok(());
69    }
70
71    // Get config path
72    let config_path = match resolve_config_path() {
73        Ok(path) => {
74            renderer.line(MessageStyle::Info, &format!("Config file: {}", path.display()))?;
75            path
76        }
77        Err(e) => {
78            renderer.line(MessageStyle::Error, &format!("Failed to determine config path: {e}"))?;
79            return Ok(());
80        }
81    };
82
83    // Step 3: Backup existing config
84    renderer.line_if_not_empty(MessageStyle::Info)?;
85
86    if config_path.exists() {
87        renderer.line(MessageStyle::Info, &format!("Creating backup of {}...", config_path.display()))?;
88
89        let backup_manager = ConfigBackupManager::new(terminal_type);
90        match backup_manager.backup_config(&config_path) {
91            Ok(backup_path) => {
92                renderer.line(MessageStyle::Status, &format!("  → Backup created: {}", backup_path.display()))?;
93            }
94            Err(e) => {
95                renderer.line(MessageStyle::Error, &format!("Failed to create backup: {e}"))?;
96                return Ok(());
97            }
98        }
99    } else {
100        renderer.line(MessageStyle::Info, &format!("Config file does not exist yet: {}", config_path.display()))?;
101        renderer.line(MessageStyle::Info, "A new config file will be created.")?;
102    }
103
104    // Step 4: Generate and apply configuration
105    renderer.line_if_not_empty(MessageStyle::Info)?;
106    renderer.line(MessageStyle::Info, "Generating configuration...")?;
107
108    // Collect enabled features
109    let enabled_features: Vec<TerminalFeature> = features
110        .iter()
111        .filter(|f| terminal_type.supports_feature(**f))
112        .copied()
113        .collect();
114
115    // Generate terminal-specific configuration
116    let new_config = match terminal_type {
117        TerminalType::Ghostty
118        | TerminalType::Kitty
119        | TerminalType::WezTerm
120        | TerminalType::Warp
121        | TerminalType::ITerm2 => {
122            anyhow::bail!("native-support terminals should return before config generation")
123        }
124        TerminalType::Alacritty => crate::terminal_setup::terminals::alacritty::generate_config(&enabled_features)?,
125        TerminalType::Zed | TerminalType::VSCode => {
126            anyhow::bail!("manual-setup terminals should return before config generation")
127        }
128        TerminalType::TerminalApp
129        | TerminalType::Xterm
130        | TerminalType::WindowsTerminal
131        | TerminalType::Hyper
132        | TerminalType::Tabby
133        | TerminalType::Unknown => {
134            anyhow::bail!("guidance-only terminals should return before config generation")
135        }
136    };
137
138    // Read existing config if it exists
139    let existing_content = if config_path.exists() {
140        read_file_with_context_sync(&config_path, "terminal config file")?
141    } else {
142        String::new()
143    };
144
145    // Merge with existing configuration
146    use crate::terminal_setup::config_writer::ConfigWriter;
147    let format = ConfigWriter::detect_format(&config_path);
148    let merged_config = ConfigWriter::merge_with_markers(&existing_content, &new_config, format)?;
149
150    // Write the configuration
151    ConfigWriter::write_atomic(&config_path, &merged_config)?;
152
153    renderer.line(MessageStyle::Status, &format!("✓ Configuration written to {}", config_path.display()))?;
154
155    // Step 5: Show completion message
156    renderer.line_if_not_empty(MessageStyle::Info)?;
157    renderer.line(MessageStyle::Status, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")?;
158    renderer.line(MessageStyle::Status, "  Setup Complete!")?;
159    renderer.line(MessageStyle::Status, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")?;
160    renderer.line_if_not_empty(MessageStyle::Info)?;
161    renderer.line(MessageStyle::Info, "Restart your terminal for changes to take effect.")?;
162
163    if config_path.exists() {
164        let backup_manager = ConfigBackupManager::new(terminal_type);
165        let backups = backup_manager.list_backups(&config_path)?;
166        if let Some(latest_backup) = backups.first() {
167            renderer.line_if_not_empty(MessageStyle::Info)?;
168            renderer.line(MessageStyle::Info, &format!("Backup saved to: {}", latest_backup.display()))?;
169        }
170    }
171
172    Ok(())
173}
174
175fn manual_setup_instructions(terminal_type: TerminalType, features: &[TerminalFeature]) -> Result<Option<String>> {
176    match terminal_type {
177        TerminalType::Zed => crate::terminal_setup::terminals::zed::generate_config(features).map(Some),
178        TerminalType::VSCode => crate::terminal_setup::terminals::vscode::generate_config(features).map(Some),
179        _ => Ok(None),
180    }
181}
182
183/// Display welcome message
184fn display_welcome(renderer: &mut AnsiRenderer) -> Result<()> {
185    renderer.line(MessageStyle::Info, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")?;
186    renderer.line(MessageStyle::Info, "  VT Code Terminal Setup Wizard")?;
187    renderer.line(MessageStyle::Info, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")?;
188    renderer.line_if_not_empty(MessageStyle::Info)?;
189    renderer.line(MessageStyle::Info, "This wizard helps you verify or configure your terminal for VT Code.")?;
190    renderer.line_if_not_empty(MessageStyle::Info)?;
191    renderer.line(MessageStyle::Info, "Features:")?;
192    renderer.line(MessageStyle::Info, "  • Shift+Enter for multiline input")?;
193    renderer.line(MessageStyle::Info, "  • Enhanced copy/paste integration")?;
194    renderer.line(MessageStyle::Info, "  • Shell integration (working directory, command status)")?;
195    renderer.line(MessageStyle::Info, "  • Theme synchronization")?;
196    renderer.line_if_not_empty(MessageStyle::Info)?;
197
198    Ok(())
199}
200
201fn render_guidance_messages(renderer: &mut AnsiRenderer, messages: &[String]) -> Result<()> {
202    renderer.line_if_not_empty(MessageStyle::Info)?;
203    for line in messages {
204        renderer.line(MessageStyle::Info, line)?;
205    }
206    Ok(())
207}
208
209fn native_terminal_setup_messages(terminal_type: TerminalType) -> Vec<String> {
210    let mut lines = vec![
211        format!(
212            "{} already supports multiline input without VT Code editing your terminal config.",
213            terminal_type.name()
214        ),
215        "Shift+Enter should work natively in this terminal.".to_string(),
216    ];
217
218    match terminal_type {
219        TerminalType::ITerm2 => {
220            lines.push("Optional macOS shortcut: set Left/Right Option to \"Esc+\" in Profiles -> Keys.".to_string());
221            lines.extend(crate::terminal_setup::features::notifications::get_notification_instructions(terminal_type));
222            lines.push(String::new());
223            lines.extend(crate::terminal_setup::terminals::iterm2::profile_icon_instructions());
224        }
225        TerminalType::Ghostty | TerminalType::Kitty | TerminalType::WezTerm => {
226            lines.extend(crate::terminal_setup::features::notifications::get_notification_instructions(terminal_type));
227        }
228        TerminalType::Warp => {
229            lines.push("Warp already provides multiline input and terminal notifications.".to_string());
230        }
231        _ => {}
232    }
233
234    lines
235}
236
237fn guidance_only_messages(terminal_type: TerminalType) -> Vec<String> {
238    match terminal_type {
239        TerminalType::TerminalApp => vec![
240            "VT Code does not auto-configure Terminal.app.".to_string(),
241            "Use Settings -> Profiles -> Keyboard and enable \"Use Option as Meta Key\" for Option+Enter workflows."
242                .to_string(),
243            "Configure notifications from Terminal -> Settings -> Profiles -> Advanced.".to_string(),
244        ],
245        TerminalType::Xterm => vec![
246            "VT Code does not auto-configure xterm.".to_string(),
247            "Configure Shift+Enter or newline shortcuts through X resources or your window manager.".to_string(),
248            "Use your terminal bell settings if you want completion alerts.".to_string(),
249        ],
250        TerminalType::WindowsTerminal => {
251            let mut lines = vec![
252                "VT Code does not currently advertise guided setup for Windows Terminal.".to_string(),
253                "Configure Shift+Enter or multiline bindings in Windows Terminal settings if you need them."
254                    .to_string(),
255                "Use the terminal bell or profile alert settings for notifications.".to_string(),
256                String::new(),
257            ];
258            lines.extend(crate::terminal_setup::terminals::windows_terminal::profile_icon_instructions());
259            lines
260        }
261        TerminalType::Hyper => vec![
262            "VT Code does not currently advertise guided setup for Hyper.".to_string(),
263            "Configure multiline bindings or plugins directly in `.hyper.js`.".to_string(),
264            "Use Hyper plugins or bell settings if you want notifications.".to_string(),
265        ],
266        TerminalType::Tabby => vec![
267            "VT Code does not currently advertise guided setup for Tabby.".to_string(),
268            "Configure multiline bindings in Tabby's terminal settings or config file.".to_string(),
269            "Use Tabby's built-in notification or bell settings if needed.".to_string(),
270        ],
271        TerminalType::Unknown => vec![
272            "Could not detect a supported terminal profile for automatic VT Code setup.".to_string(),
273            "Use \\ + Enter for multiline input, or configure your terminal to send a newline on Shift+Enter."
274                .to_string(),
275            "On macOS, Option+Enter is often the simplest fallback once Option is configured as Meta.".to_string(),
276        ],
277        _ => vec![format!(
278            "VT Code does not currently offer guided setup for {}.",
279            terminal_type.name()
280        )],
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::{guidance_only_messages, native_terminal_setup_messages};
287    use crate::terminal_setup::detector::TerminalType;
288
289    #[test]
290    fn test_wizard_module() {
291        // Placeholder test - actual wizard tests would need mocked terminal I/O
292    }
293
294    #[test]
295    fn native_setup_messages_are_noop_guidance() {
296        let lines = native_terminal_setup_messages(TerminalType::WezTerm);
297        assert!(lines.iter().any(|line| line.contains("already supports multiline")));
298        assert!(lines.iter().any(|line| line.contains("Shift+Enter")));
299    }
300
301    #[test]
302    fn iterm2_native_messages_advertise_profile_icon() {
303        let iterm_lines = native_terminal_setup_messages(TerminalType::ITerm2);
304        assert!(iterm_lines.iter().any(|line| line.contains("install-iterm2-icon")));
305
306        let wezterm_lines = native_terminal_setup_messages(TerminalType::WezTerm);
307        assert!(!wezterm_lines.iter().any(|line| line.contains("install-iterm2-icon")));
308    }
309
310    #[test]
311    fn guidance_only_messages_cover_terminal_app() {
312        let lines = guidance_only_messages(TerminalType::TerminalApp);
313        assert!(lines.iter().any(|line| line.contains("does not auto-configure")));
314        assert!(lines.iter().any(|line| line.contains("Use Option as Meta Key")));
315    }
316
317    #[test]
318    fn windows_terminal_guidance_mentions_profile_icon() {
319        let lines = guidance_only_messages(TerminalType::WindowsTerminal);
320        let joined = lines.join("\n");
321        assert!(joined.contains("icon"));
322        assert!(joined.contains(".png"));
323    }
324    #[test]
325    fn zed_manual_setup_returns_before_path_resolution_or_file_operations() {
326        use crate::utils::ansi::AnsiRenderer;
327        use vtcode_ui::tui::core_tui::app::types::InlineHandle;
328        let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
329        let mut renderer = AnsiRenderer::with_inline_ui(InlineHandle::new_for_tests(sender), Default::default());
330        let sandbox = tempfile::tempdir().unwrap();
331        let keymap = sandbox.path().join("keymap.json");
332        std::fs::write(&keymap, "[{\"context\":\"Editor\",\"bindings\":{}}]").unwrap();
333        super::run_setup_for_terminal(&mut renderer, TerminalType::Zed, || {
334            panic!("manual setup must return before resolving any config path");
335        })
336        .unwrap();
337        assert_eq!(std::fs::read_to_string(&keymap).unwrap(), "[{\"context\":\"Editor\",\"bindings\":{}}]");
338        assert_eq!(std::fs::read_dir(sandbox.path()).unwrap().count(), 1);
339        assert!(receiver.try_recv().is_ok(), "setup must emit instructions");
340        let instructions = super::manual_setup_instructions(TerminalType::Zed, &[super::TerminalFeature::Multiline])
341            .unwrap()
342            .unwrap();
343        assert!(instructions.contains("existing keymap array"));
344        assert!(instructions.contains("terminal::SendText"));
345        assert!(
346            super::manual_setup_instructions(TerminalType::Alacritty, &[])
347                .unwrap()
348                .is_none()
349        );
350    }
351}