vtcode_core/terminal_setup/
wizard.rs1use 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
13pub async fn run_terminal_setup_wizard(
15 renderer: &mut AnsiRenderer,
16 _config: &VTCodeConfig,
17) -> Result<()> {
18 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 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 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 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 renderer.line_if_not_empty(MessageStyle::Info)?;
122 renderer.line(MessageStyle::Info, "Generating configuration...")?;
123
124 let enabled_features: Vec<TerminalFeature> = features
126 .iter()
127 .filter(|f| terminal_type.supports_feature(**f))
128 .copied()
129 .collect();
130
131 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 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 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 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 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 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
216fn 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 }
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}