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