1use clap::{Args, Subcommand, ValueEnum};
2use clap_complete::Shell;
3use std::env;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7use crate::config::{home_dir, write_text_file};
8use crate::AppError;
9
10const MANAGED_BLOCK_START: &str = "# >>> cc-switch completions >>>";
11const MANAGED_BLOCK_END: &str = "# <<< cc-switch completions <<<";
12const COMMAND_NAME: &str = "cc-switch-tui";
13
14#[derive(Args, Debug, Clone)]
15#[command(arg_required_else_help = true)]
16pub struct CompletionsCommand {
17 #[arg(value_enum)]
19 pub shell: Option<Shell>,
20
21 #[command(subcommand)]
22 pub action: Option<CompletionsAction>,
23}
24
25#[derive(Subcommand, Debug, Clone)]
26pub enum CompletionsAction {
27 Install(CompletionsInstallCommand),
29
30 Status(CompletionLifecycleCommand),
32
33 Uninstall(CompletionLifecycleCommand),
35}
36
37#[derive(Args, Debug, Clone)]
38pub struct CompletionsInstallCommand {
39 #[arg(long, value_enum, default_value_t = ManagedShellSelection::Auto)]
41 pub shell: ManagedShellSelection,
42
43 #[arg(long)]
45 pub activate: bool,
46}
47
48#[derive(Args, Debug, Clone)]
49pub struct CompletionLifecycleCommand {
50 #[arg(long, value_enum, default_value_t = ManagedShellSelection::Auto)]
52 pub shell: ManagedShellSelection,
53}
54
55#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
56pub enum ManagedShellSelection {
57 Auto,
58 Bash,
59 Zsh,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63enum ManagedShell {
64 Bash,
65 Zsh,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
69struct CompletionPaths {
70 completion_file: PathBuf,
71 completion_dir: PathBuf,
72 rc_file: PathBuf,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76enum ActivationChange {
77 Added,
78 Updated,
79 Unchanged,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83struct StatusSnapshot {
84 shell: ManagedShell,
85 shell_message: String,
86 paths: CompletionPaths,
87 completion_file_exists: bool,
88 rc_file_exists: bool,
89 activation_managed: bool,
90 reload_recommended: bool,
91 next_step: String,
92}
93
94pub fn execute(command: CompletionsCommand) -> Result<(), AppError> {
95 match (command.shell, command.action) {
96 (Some(shell), None) => {
97 crate::cli::generate_completions(shell);
98 Ok(())
99 }
100 (None, Some(CompletionsAction::Install(args))) => execute_install(args),
101 (None, Some(CompletionsAction::Status(args))) => execute_status(args),
102 (None, Some(CompletionsAction::Uninstall(args))) => execute_uninstall(args),
103 (Some(_), Some(_)) => Err(AppError::InvalidInput(
104 "shell generator path cannot be combined with lifecycle subcommands".to_string(),
105 )),
106 (None, None) => Err(AppError::InvalidInput(
107 "missing completions shell or lifecycle subcommand".to_string(),
108 )),
109 }
110}
111
112fn execute_install(args: CompletionsInstallCommand) -> Result<(), AppError> {
113 let (shell, shell_message) = resolve_managed_shell(args.shell)?;
114 let home = require_home_dir()?;
115 let paths = completion_paths(shell, &home);
116 let script = generated_completion_script(shell)?;
117
118 write_text_file(&paths.completion_file, &script)?;
119
120 println!("{shell_message}");
121 println!(
122 "Installed completion file: {}",
123 paths.completion_file.display()
124 );
125
126 if args.activate {
127 let activation_change = upsert_activation_block(shell, &paths)?;
128 println!(
129 "Managed activation block: {}",
130 match activation_change {
131 ActivationChange::Added => format!("added to {}", paths.rc_file.display()),
132 ActivationChange::Updated => format!("updated in {}", paths.rc_file.display()),
133 ActivationChange::Unchanged => {
134 format!("already present in {}", paths.rc_file.display())
135 }
136 }
137 );
138 println!(
139 "Next step: reload your shell or run `{}`",
140 reload_command(shell, &paths)
141 );
142 } else {
143 println!(
144 "Managed activation block: not changed (use --activate to let cc-switch edit {})",
145 paths.rc_file.display()
146 );
147 println!(
148 "Next step: run `{COMMAND_NAME} completions install --shell {} --activate` to activate it automatically, or wire {} manually.",
149 shell.as_str(),
150 paths.rc_file.display()
151 );
152 }
153
154 Ok(())
155}
156
157fn execute_status(args: CompletionLifecycleCommand) -> Result<(), AppError> {
158 let snapshot = completion_status(args.shell)?;
159
160 println!("{}", snapshot.shell_message);
161 println!(
162 "Completion file: {} ({})",
163 snapshot.paths.completion_file.display(),
164 if snapshot.completion_file_exists {
165 "installed"
166 } else {
167 "missing"
168 }
169 );
170 println!(
171 "RC file: {} ({})",
172 snapshot.paths.rc_file.display(),
173 if snapshot.rc_file_exists {
174 "present"
175 } else {
176 "missing"
177 }
178 );
179 println!(
180 "Managed activation block: {}",
181 if snapshot.activation_managed {
182 "yes"
183 } else {
184 "no"
185 }
186 );
187 println!(
188 "Current shell likely needs reload: {}",
189 if snapshot.reload_recommended {
190 "yes"
191 } else {
192 "no"
193 }
194 );
195 println!("Next step: {}", snapshot.next_step);
196
197 Ok(())
198}
199
200fn execute_uninstall(args: CompletionLifecycleCommand) -> Result<(), AppError> {
201 let snapshot = completion_status(args.shell)?;
202
203 if snapshot.completion_file_exists {
204 fs::remove_file(&snapshot.paths.completion_file)
205 .map_err(|err| AppError::io(&snapshot.paths.completion_file, err))?;
206 }
207
208 let activation_removed = remove_activation_block(&snapshot.paths)?;
209
210 println!("{}", snapshot.shell_message);
211 println!(
212 "Removed completion file: {}",
213 if snapshot.completion_file_exists {
214 snapshot.paths.completion_file.display().to_string()
215 } else {
216 format!(
217 "{} (already absent)",
218 snapshot.paths.completion_file.display()
219 )
220 }
221 );
222 println!(
223 "Managed activation block: {}",
224 if activation_removed {
225 format!("removed from {}", snapshot.paths.rc_file.display())
226 } else {
227 format!("not present in {}", snapshot.paths.rc_file.display())
228 }
229 );
230 println!(
231 "Next step: reload your shell or restart it to drop any already-loaded completion definitions."
232 );
233
234 Ok(())
235}
236
237fn completion_status(selection: ManagedShellSelection) -> Result<StatusSnapshot, AppError> {
238 let (shell, shell_message) = resolve_managed_shell(selection)?;
239 let home = require_home_dir()?;
240 let paths = completion_paths(shell, &home);
241 let rc_content = read_optional_text_file(&paths.rc_file)?;
242 let activation_managed = managed_block_range(&rc_content).is_some();
243 let completion_file_exists = paths.completion_file.exists();
244
245 Ok(StatusSnapshot {
246 shell,
247 shell_message,
248 rc_file_exists: paths.rc_file.exists(),
249 completion_file_exists,
250 activation_managed,
251 reload_recommended: completion_file_exists && activation_managed,
252 next_step: next_step_for_status(shell, &paths, completion_file_exists, activation_managed),
253 paths,
254 })
255}
256
257fn next_step_for_status(
258 shell: ManagedShell,
259 paths: &CompletionPaths,
260 completion_file_exists: bool,
261 activation_managed: bool,
262) -> String {
263 if !completion_file_exists {
264 return format!(
265 "Run `{COMMAND_NAME} completions install --shell {}` to install the completion file.",
266 shell.as_str()
267 );
268 }
269
270 if !activation_managed {
271 return format!(
272 "Run `{COMMAND_NAME} completions install --shell {} --activate` to add a managed activation block to {}.",
273 shell.as_str(),
274 paths.rc_file.display()
275 );
276 }
277
278 format!(
279 "Reload your shell or run `{}`.",
280 reload_command(shell, paths)
281 )
282}
283
284fn resolve_managed_shell(
285 selection: ManagedShellSelection,
286) -> Result<(ManagedShell, String), AppError> {
287 match selection {
288 ManagedShellSelection::Bash => Ok((ManagedShell::Bash, "Selected shell: bash".to_string())),
289 ManagedShellSelection::Zsh => Ok((ManagedShell::Zsh, "Selected shell: zsh".to_string())),
290 ManagedShellSelection::Auto => detect_shell_from_env(),
291 }
292}
293
294fn detect_shell_from_env() -> Result<(ManagedShell, String), AppError> {
295 let shell_env = env::var("SHELL").ok();
296 let shell_name = shell_env
297 .as_deref()
298 .and_then(|value| Path::new(value).file_name())
299 .and_then(|value| value.to_str());
300
301 match shell_name {
302 Some("bash") => Ok((
303 ManagedShell::Bash,
304 format!(
305 "Detected shell: bash{}",
306 shell_env
307 .as_deref()
308 .map(|value| format!(" (from SHELL={value})"))
309 .unwrap_or_default()
310 ),
311 )),
312 Some("zsh") => Ok((
313 ManagedShell::Zsh,
314 format!(
315 "Detected shell: zsh{}",
316 shell_env
317 .as_deref()
318 .map(|value| format!(" (from SHELL={value})"))
319 .unwrap_or_default()
320 ),
321 )),
322 Some(other) => Err(AppError::Message(format!(
323 "Automatic completion lifecycle currently supports only bash and zsh. Detected SHELL={other}. Re-run with `--shell bash` or `--shell zsh`, or use raw generation via `{COMMAND_NAME} completions <shell>`."
324 ))),
325 None => Err(AppError::Message(format!(
326 "Could not detect a supported shell from SHELL. Re-run with `--shell bash` or `--shell zsh`, or use raw generation via `{COMMAND_NAME} completions <shell>`."
327 ))),
328 }
329}
330
331fn require_home_dir() -> Result<PathBuf, AppError> {
332 home_dir().ok_or_else(|| AppError::Config("无法获取用户主目录".to_string()))
333}
334
335fn completion_paths(shell: ManagedShell, home: &Path) -> CompletionPaths {
336 match shell {
337 ManagedShell::Bash => CompletionPaths {
338 completion_file: home
339 .join(".local/share/bash-completion/completions")
340 .join(COMMAND_NAME),
341 completion_dir: home.join(".local/share/bash-completion/completions"),
342 rc_file: home.join(".bashrc"),
343 },
344 ManagedShell::Zsh => CompletionPaths {
345 completion_file: home
346 .join(".local/share/zsh/site-functions")
347 .join(format!("_{COMMAND_NAME}")),
348 completion_dir: home.join(".local/share/zsh/site-functions"),
349 rc_file: home.join(".zshrc"),
350 },
351 }
352}
353
354fn generated_completion_script(shell: ManagedShell) -> Result<String, AppError> {
355 let mut buffer = Vec::new();
356 crate::cli::generate_completions_to(shell.generator_shell(), &mut buffer);
357 String::from_utf8(buffer)
358 .map_err(|err| AppError::Message(format!("Failed to decode generated completions: {err}")))
359}
360
361fn upsert_activation_block(
362 shell: ManagedShell,
363 paths: &CompletionPaths,
364) -> Result<ActivationChange, AppError> {
365 let existing = read_optional_text_file(&paths.rc_file)?;
366 let (stripped, had_block) = remove_managed_block(&existing);
367 let insert_before = match shell {
368 ManagedShell::Zsh => find_compinit_insert_offset(&stripped),
369 ManagedShell::Bash => None,
370 };
371 let block = activation_block(shell, insert_before.is_none());
372 let updated = insert_block(&stripped, &block, insert_before);
373
374 if updated != existing {
375 write_text_file(&paths.rc_file, &updated)?;
376 }
377
378 Ok(match (had_block, updated == existing) {
379 (false, _) => ActivationChange::Added,
380 (true, true) => ActivationChange::Unchanged,
381 (true, false) => ActivationChange::Updated,
382 })
383}
384
385fn remove_activation_block(paths: &CompletionPaths) -> Result<bool, AppError> {
386 let existing = read_optional_text_file(&paths.rc_file)?;
387 let (updated, had_block) = remove_managed_block(&existing);
388
389 if had_block && updated != existing {
390 write_text_file(&paths.rc_file, &updated)?;
391 }
392
393 Ok(had_block)
394}
395
396fn read_optional_text_file(path: &Path) -> Result<String, AppError> {
397 match fs::read_to_string(path) {
398 Ok(content) => Ok(content),
399 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(String::new()),
400 Err(err) => Err(AppError::io(path, err)),
401 }
402}
403
404fn managed_block_range(content: &str) -> Option<(usize, usize)> {
405 let start = content.find(MANAGED_BLOCK_START)?;
406 let end_start = content[start..].find(MANAGED_BLOCK_END)? + start;
407 let mut end = end_start + MANAGED_BLOCK_END.len();
408
409 if content[end..].starts_with("\r\n") {
410 end += 2;
411 } else if content[end..].starts_with('\n') {
412 end += 1;
413 }
414
415 Some((start, end))
416}
417
418fn remove_managed_block(content: &str) -> (String, bool) {
419 let Some((start, end)) = managed_block_range(content) else {
420 return (content.to_string(), false);
421 };
422
423 let mut updated = String::with_capacity(content.len());
424 updated.push_str(&content[..start]);
425 updated.push_str(&content[end..]);
426
427 (updated, true)
428}
429
430fn find_compinit_insert_offset(content: &str) -> Option<usize> {
431 let mut offset = 0usize;
432
433 for line in content.split_inclusive('\n') {
434 let trimmed = line.trim_start();
435 if !trimmed.starts_with('#') && trimmed.contains("compinit") {
436 return Some(offset);
437 }
438 offset += line.len();
439 }
440
441 None
442}
443
444fn insert_block(content: &str, block: &str, before: Option<usize>) -> String {
445 let mut updated = String::new();
446
447 match before {
448 Some(index) => {
449 updated.push_str(&content[..index]);
450 if !updated.is_empty() && !updated.ends_with('\n') {
451 updated.push('\n');
452 }
453 updated.push_str(block);
454 if !updated.ends_with('\n') {
455 updated.push('\n');
456 }
457 if !content[index..].is_empty() && !content[index..].starts_with('\n') {
458 updated.push('\n');
459 }
460 updated.push_str(&content[index..]);
461 }
462 None => {
463 updated.push_str(content);
464 if !updated.is_empty() && !updated.ends_with('\n') {
465 updated.push('\n');
466 }
467 updated.push_str(block);
468 if !updated.ends_with('\n') {
469 updated.push('\n');
470 }
471 }
472 }
473
474 updated
475}
476
477fn activation_block(shell: ManagedShell, bootstrap_compinit: bool) -> String {
478 match shell {
479 ManagedShell::Bash => format!(
480 "{MANAGED_BLOCK_START}\nif [ -f \"$HOME/.local/share/bash-completion/completions/{COMMAND_NAME}\" ]; then\n . \"$HOME/.local/share/bash-completion/completions/{COMMAND_NAME}\"\nfi\n{MANAGED_BLOCK_END}\n"
481 ),
482 ManagedShell::Zsh if bootstrap_compinit => format!(
483 "{MANAGED_BLOCK_START}\nif [ -d \"$HOME/.local/share/zsh/site-functions\" ]; then\n fpath=(\"$HOME/.local/share/zsh/site-functions\" $fpath)\nfi\nautoload -Uz compinit\ncompinit\n{MANAGED_BLOCK_END}\n"
484 ),
485 ManagedShell::Zsh => format!(
486 "{MANAGED_BLOCK_START}\nif [ -d \"$HOME/.local/share/zsh/site-functions\" ]; then\n fpath=(\"$HOME/.local/share/zsh/site-functions\" $fpath)\nfi\n{MANAGED_BLOCK_END}\n"
487 ),
488 }
489}
490
491fn reload_command(shell: ManagedShell, paths: &CompletionPaths) -> String {
492 match shell {
493 ManagedShell::Bash | ManagedShell::Zsh => format!("source {}", paths.rc_file.display()),
494 }
495}
496
497impl ManagedShell {
498 fn as_str(self) -> &'static str {
499 match self {
500 Self::Bash => "bash",
501 Self::Zsh => "zsh",
502 }
503 }
504
505 fn generator_shell(self) -> Shell {
506 match self {
507 Self::Bash => Shell::Bash,
508 Self::Zsh => Shell::Zsh,
509 }
510 }
511}
512
513#[cfg(test)]
514mod tests {
515 use super::*;
516 use crate::test_support::{lock_test_home_and_settings, set_test_home_override};
517 use serial_test::serial;
518 use std::ffi::OsString;
519
520 struct ShellEnvGuard {
521 original: Option<OsString>,
522 }
523
524 impl ShellEnvGuard {
525 fn set(value: Option<&str>) -> Self {
526 let original = env::var_os("SHELL");
527 match value {
528 Some(value) => unsafe { env::set_var("SHELL", value) },
529 None => unsafe { env::remove_var("SHELL") },
530 }
531 Self { original }
532 }
533 }
534
535 impl Drop for ShellEnvGuard {
536 fn drop(&mut self) {
537 match self.original.as_ref() {
538 Some(value) => unsafe { env::set_var("SHELL", value) },
539 None => unsafe { env::remove_var("SHELL") },
540 }
541 }
542 }
543
544 struct TestHomeGuard {
545 _lock: crate::test_support::TestHomeSettingsLock,
546 old_shell: ShellEnvGuard,
547 }
548
549 impl TestHomeGuard {
550 fn new(home: &Path, shell: Option<&str>) -> Self {
551 let lock = lock_test_home_and_settings();
552 set_test_home_override(Some(home));
553 let old_shell = ShellEnvGuard::set(shell);
554 Self {
555 _lock: lock,
556 old_shell,
557 }
558 }
559 }
560
561 impl Drop for TestHomeGuard {
562 fn drop(&mut self) {
563 let _ = &self.old_shell;
564 set_test_home_override(None);
565 }
566 }
567
568 fn marker_count(content: &str) -> usize {
569 content.matches(MANAGED_BLOCK_START).count()
570 }
571
572 #[test]
573 #[serial(home_settings)]
574 fn detects_supported_shell_from_env() {
575 let _guard = ShellEnvGuard::set(Some("/bin/bash"));
576 let (shell, message) =
577 resolve_managed_shell(ManagedShellSelection::Auto).expect("bash should resolve");
578 assert_eq!(shell, ManagedShell::Bash);
579 assert!(message.contains("Detected shell: bash"));
580 }
581
582 #[test]
583 #[serial(home_settings)]
584 fn auto_shell_detection_rejects_unsupported_shell() {
585 let _guard = ShellEnvGuard::set(Some("/usr/bin/fish"));
586 let err = resolve_managed_shell(ManagedShellSelection::Auto)
587 .expect_err("fish should not be supported for managed lifecycle");
588 assert!(err.to_string().contains("only bash and zsh"));
589 }
590
591 #[test]
592 #[serial(home_settings)]
593 fn auto_shell_detection_requires_shell_env() {
594 let _guard = ShellEnvGuard::set(None);
595 let err = resolve_managed_shell(ManagedShellSelection::Auto)
596 .expect_err("missing SHELL should fail");
597 assert!(err.to_string().contains("Could not detect"));
598 }
599
600 #[test]
601 fn calculates_bash_and_zsh_completion_paths_under_home_override() {
602 let home = PathBuf::from("/tmp/cc-switch-home");
603
604 let bash = completion_paths(ManagedShell::Bash, &home);
605 assert_eq!(
606 bash.completion_file,
607 home.join(".local/share/bash-completion/completions/cc-switch-tui")
608 );
609 assert_eq!(bash.rc_file, home.join(".bashrc"));
610
611 let zsh = completion_paths(ManagedShell::Zsh, &home);
612 assert_eq!(
613 zsh.completion_file,
614 home.join(".local/share/zsh/site-functions/_cc-switch-tui")
615 );
616 assert_eq!(zsh.rc_file, home.join(".zshrc"));
617 }
618
619 #[test]
620 #[serial(home_settings)]
621 fn install_with_activate_creates_completion_file_and_managed_bash_block() {
622 let temp = tempfile::tempdir().expect("create temp home");
623 let _guard = TestHomeGuard::new(temp.path(), Some("/bin/bash"));
624
625 execute_install(CompletionsInstallCommand {
626 shell: ManagedShellSelection::Auto,
627 activate: true,
628 })
629 .expect("install should succeed");
630
631 let paths = completion_paths(ManagedShell::Bash, temp.path());
632 let script = fs::read_to_string(&paths.completion_file).expect("read bash completion");
633 let rc = fs::read_to_string(&paths.rc_file).expect("read bash rc");
634
635 assert!(script.contains("_cc-switch-tui"));
636 assert_eq!(marker_count(&rc), 1);
637 assert!(rc.contains(". \"$HOME/.local/share/bash-completion/completions/cc-switch-tui\""));
638 }
639
640 #[test]
641 #[serial(home_settings)]
642 fn activate_is_idempotent_for_bash() {
643 let temp = tempfile::tempdir().expect("create temp home");
644 let _guard = TestHomeGuard::new(temp.path(), Some("/bin/bash"));
645
646 execute_install(CompletionsInstallCommand {
647 shell: ManagedShellSelection::Auto,
648 activate: true,
649 })
650 .expect("first install should succeed");
651 execute_install(CompletionsInstallCommand {
652 shell: ManagedShellSelection::Auto,
653 activate: true,
654 })
655 .expect("second install should succeed");
656
657 let paths = completion_paths(ManagedShell::Bash, temp.path());
658 let rc = fs::read_to_string(&paths.rc_file).expect("read bash rc");
659 assert_eq!(marker_count(&rc), 1);
660 }
661
662 #[test]
663 #[serial(home_settings)]
664 fn uninstall_removes_only_managed_block_and_keeps_surrounding_bash_content() {
665 let temp = tempfile::tempdir().expect("create temp home");
666 let _guard = TestHomeGuard::new(temp.path(), Some("/bin/bash"));
667 let paths = completion_paths(ManagedShell::Bash, temp.path());
668 write_text_file(
669 &paths.rc_file,
670 "export PATH=\"$HOME/.local/bin:$PATH\"\n\n# >>> cc-switch completions >>>\nlegacy\n# <<< cc-switch completions <<<\n\nalias ll='ls -al'\n",
671 )
672 .expect("write bash rc");
673
674 execute_uninstall(CompletionLifecycleCommand {
675 shell: ManagedShellSelection::Auto,
676 })
677 .expect("uninstall should succeed");
678
679 let rc = fs::read_to_string(&paths.rc_file).expect("read bash rc");
680 assert!(!rc.contains(MANAGED_BLOCK_START));
681 assert!(rc.contains("export PATH=\"$HOME/.local/bin:$PATH\""));
682 assert!(rc.contains("alias ll='ls -al'"));
683 }
684
685 #[test]
686 #[serial(home_settings)]
687 fn zsh_activate_inserts_managed_block_before_existing_compinit() {
688 let temp = tempfile::tempdir().expect("create temp home");
689 let _guard = TestHomeGuard::new(temp.path(), Some("/bin/zsh"));
690 let paths = completion_paths(ManagedShell::Zsh, temp.path());
691 write_text_file(
692 &paths.rc_file,
693 "export PATH=\"$HOME/.local/bin:$PATH\"\nautoload -Uz compinit\ncompinit\n",
694 )
695 .expect("seed zshrc");
696
697 execute_install(CompletionsInstallCommand {
698 shell: ManagedShellSelection::Auto,
699 activate: true,
700 })
701 .expect("install should succeed");
702
703 let rc = fs::read_to_string(&paths.rc_file).expect("read zshrc");
704 let block_pos = rc.find(MANAGED_BLOCK_START).expect("managed block exists");
705 let compinit_pos = rc.find("autoload -Uz compinit").expect("compinit exists");
706 assert!(block_pos < compinit_pos);
707 assert!(!rc[block_pos..compinit_pos].contains("compinit\ncompinit"));
708 }
709
710 #[test]
711 #[serial(home_settings)]
712 fn zsh_activate_bootstraps_compinit_when_missing() {
713 let temp = tempfile::tempdir().expect("create temp home");
714 let _guard = TestHomeGuard::new(temp.path(), Some("/bin/zsh"));
715
716 execute_install(CompletionsInstallCommand {
717 shell: ManagedShellSelection::Auto,
718 activate: true,
719 })
720 .expect("install should succeed");
721
722 let paths = completion_paths(ManagedShell::Zsh, temp.path());
723 let rc = fs::read_to_string(&paths.rc_file).expect("read zshrc");
724 assert!(rc.contains("autoload -Uz compinit"));
725 assert!(rc.contains("\ncompinit\n"));
726 }
727
728 #[test]
729 #[serial(home_settings)]
730 fn status_reports_missing_activation_when_install_did_not_edit_rc() {
731 let temp = tempfile::tempdir().expect("create temp home");
732 let _guard = TestHomeGuard::new(temp.path(), Some("/bin/bash"));
733
734 execute_install(CompletionsInstallCommand {
735 shell: ManagedShellSelection::Auto,
736 activate: false,
737 })
738 .expect("install should succeed");
739
740 let snapshot = completion_status(ManagedShellSelection::Auto).expect("status");
741 assert!(snapshot.completion_file_exists);
742 assert!(!snapshot.activation_managed);
743 assert!(snapshot.next_step.contains("--activate"));
744 }
745}