1use 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
9const STANDARD_SHELL_FILES: &[&str] = &[".bashrc", ".zshrc", ".kshrc", "config.fish", ".profile", ".zprofile"];
11
12pub fn run_init_wizard() -> Result<()> {
14 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 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 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 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 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
121pub 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
199pub(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;