Skip to main content

alf/cli/
init.rs

1//! First-run initialization wizard for alf.
2
3use crate::config::{is_first_run, save_config, Config, GeneralConfig, UiConfig};
4use crate::tui::themes::Theme;
5use anyhow::Result;
6use std::io::{self, Write};
7use std::path::PathBuf;
8
9/// Standard shell configuration files to check
10const STANDARD_SHELL_FILES: &[&str] = &[".bashrc", ".zshrc", ".kshrc", "config.fish", ".profile", ".zprofile"];
11
12/// Run the initialization wizard
13pub fn run_init_wizard() -> Result<()> {
14   // Check if already configured
15   if !is_first_run()? {
16      eprintln!("Config already exists at $HOME/.config/alf/config.toml");
17      eprintln!("To reconfigure, run: alf config reset");
18      return Ok(());
19   }
20
21   println!("Welcome to alf!\n");
22
23   // Auto-detect standard shell files
24   let home = std::env::var("HOME").map_err(|_| {
25      let _ = anyhow::anyhow!("HOME environment variable not set");
26   });
27   let detected_files = detect_shell_files(&home.unwrap());
28
29   println!("Detected shell files:");
30   if detected_files.is_empty() {
31      println!("  (none found)");
32   } else {
33      for file in &detected_files {
34         println!("  ✓ {}", file);
35      }
36   }
37   println!();
38
39   // Prompt for additional files
40   print!("Additional files? (comma-separated paths, or Enter to skip):\n> ");
41   io::stdout().flush()?;
42
43   let mut additional = String::new();
44   io::stdin().read_line(&mut additional)?;
45
46   let mut all_files = detected_files;
47   if !additional.trim().is_empty() {
48      for path in additional.split(',') {
49         let trimmed = path.trim();
50         if !trimmed.is_empty() {
51            all_files.push(trimmed.to_string());
52         }
53      }
54   }
55
56   println!();
57
58   // Theme selection
59   println!("Choose a theme:");
60   let themes = Theme::available_themes();
61   for (i, theme_name) in themes.iter().enumerate() {
62      println!("  {}) {}", i + 1, theme_name);
63   }
64
65   print!("> ");
66   io::stdout().flush()?;
67
68   let mut choice = String::new();
69   io::stdin().read_line(&mut choice)?;
70
71   let theme_idx = choice.trim().parse::<usize>().unwrap_or(1).saturating_sub(1);
72   let selected_theme = themes.get(theme_idx).cloned().unwrap_or_else(|| {
73      eprintln!("Invalid selection, using default theme.");
74      "default".to_string()
75   });
76
77   println!();
78
79   // Create and save config
80   let config = Config {
81      general: GeneralConfig {
82         shell_files: all_files,
83         ..Default::default()
84      },
85      ui: UiConfig {
86         theme: selected_theme,
87         keybind_mode: "vim".to_string(),
88      },
89      ..Default::default()
90   };
91
92   save_config(&config)?;
93
94   let config_path = crate::config::get_config_path()?;
95   println!("Config saved to {}", config_path.display());
96   println!();
97   println!("Shell integration");
98   println!("─────────────────");
99   println!("Add the following to your shell config to enable command-line population.");
100   println!("This installs the `alf` command wrapper.\n");
101
102   println!("For zsh (add to ~/.zshrc):");
103   println!("{}\n", get_shell_hook("zsh"));
104
105   println!("For bash (add to ~/.bashrc):");
106   println!("{}\n", get_shell_hook("bash"));
107
108   println!("Or run: eval \"$(alf activate <zsh|bash>)\"");
109   println!();
110   println!("Usage:");
111   println!("  - Type `alf` at the prompt to open the picker.");
112   println!("  - In the TUI, Tab populates the prompt with the entry; Enter runs it.");
113   println!("  - Note: in bash, Tab cannot populate the readline buffer;");
114   println!("    it will print the entry instead.");
115   println!();
116   println!("Run `alf` to start.");
117
118   Ok(())
119}
120
121/// Print the shell integration wrapper for a given shell
122pub fn print_shell_hook(shell: &str) -> Result<()> {
123   match shell.to_lowercase().as_str() {
124      "zsh" | "bash" => {
125         println!("{}", get_shell_hook(shell));
126         Ok(())
127      },
128      _ => {
129         eprintln!("Unsupported shell: {}. Use 'zsh' or 'bash'.", shell);
130         Err(anyhow::anyhow!("Unsupported shell: {}", shell))
131      },
132   }
133}
134
135fn get_shell_hook(shell: &str) -> &'static str {
136   match shell.to_lowercase().as_str() {
137      "zsh" => {
138         r#"alf() {
139  local tmp action entry rc
140  tmp="$(mktemp)" || return 1
141  ALF_OUTPUT="$tmp" command alf "$@"
142  rc=$?
143  if [[ -s "$tmp" ]]; then
144    action="$(sed -n '1p' "$tmp")"
145    entry="$(sed -n '2p' "$tmp")"
146    rm -f "$tmp"
147    if [[ -n "$entry" ]]; then
148      if [[ "$action" == "execute" ]]; then
149        print -s -- "$entry"
150        fc -A
151        if (( ${+functions[_atuin_preexec]} )); then
152          _atuin_preexec "$entry"
153          eval -- "$entry"
154          _atuin_precmd
155        else
156          eval -- "$entry"
157        fi
158        return
159      else
160        print -z -- "$entry"
161      fi
162    fi
163  else
164    rm -f "$tmp"
165  fi
166  return $rc
167}"#
168      },
169      "bash" => {
170         r#"alf() {
171  local tmp action entry rc
172  tmp="$(mktemp)" || return 1
173  ALF_OUTPUT="$tmp" command alf "$@"
174  rc=$?
175  if [[ -s "$tmp" ]]; then
176    action="$(sed -n '1p' "$tmp")"
177    entry="$(sed -n '2p' "$tmp")"
178    rm -f "$tmp"
179    if [[ -n "$entry" ]]; then
180      if [[ "$action" == "execute" ]]; then
181        history -s -- "$entry"
182        history -a
183        eval -- "$entry"
184        return
185      else
186        printf '%s\n' "$entry"
187      fi
188    fi
189  else
190    rm -f "$tmp"
191  fi
192  return $rc
193}"#
194      },
195      _ => "",
196   }
197}
198
199/// Detect which standard shell files exist in the home directory
200pub(super) fn detect_shell_files(home: &str) -> Vec<String> {
201   STANDARD_SHELL_FILES
202      .iter()
203      .filter_map(|filename| {
204         let mut path = PathBuf::from(home).join(filename);
205
206         if *filename == "config.fish" {
207            path = PathBuf::from(home).join(".config/fish").join(filename);
208         }
209
210         if path.exists() {
211            Some(path.to_string_lossy().to_string())
212         } else {
213            None
214         }
215      })
216      .collect()
217}
218
219#[cfg(test)]
220#[path = "init_tests.rs"]
221mod init_tests;