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(renderer: &mut AnsiRenderer, _config: &VTCodeConfig) -> Result<()> {
15 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 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 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 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 renderer.line_if_not_empty(MessageStyle::Info)?;
106 renderer.line(MessageStyle::Info, "Generating configuration...")?;
107
108 let enabled_features: Vec<TerminalFeature> = features
110 .iter()
111 .filter(|f| terminal_type.supports_feature(**f))
112 .copied()
113 .collect();
114
115 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 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 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 ConfigWriter::write_atomic(&config_path, &merged_config)?;
152
153 renderer.line(MessageStyle::Status, &format!("✓ Configuration written to {}", config_path.display()))?;
154
155 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
183fn 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 }
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}