Skip to main content

par_term/cli/
install.rs

1//! CLI install, uninstall, and self-update procedure implementations.
2//!
3//! These functions are invoked by [`super::process_cli`] when the user runs
4//! install/uninstall/self-update subcommands. Each follows a
5//! prompt-confirm-execute-report pattern and is the only place in the codebase
6//! that uses blocking stdin reads.
7
8use std::io::{self, Write};
9
10use crate::config::ShellType;
11use crate::shader_installer;
12use crate::shell_integration_installer;
13
14/// Install shaders from the latest GitHub release (CLI version with prompts and output)
15pub fn install_shaders_cli(skip_prompt: bool) -> anyhow::Result<()> {
16    let shaders_dir = crate::config::Config::shaders_dir();
17
18    println!("=============================================");
19    println!("  par-term Shader Installer");
20    println!("=============================================");
21    println!();
22    println!("Target directory: {}", shaders_dir.display());
23    println!();
24
25    // Check if directory has existing shaders
26    if shaders_dir.exists() && shader_installer::has_shader_files(&shaders_dir) && !skip_prompt {
27        println!("WARNING: This will overwrite existing shaders in:");
28        println!("  {}", shaders_dir.display());
29        println!();
30        print!("Do you want to continue? [y/N] ");
31        io::stdout().flush()?;
32
33        let mut response = String::new();
34        io::stdin().read_line(&mut response)?;
35        let response = response.trim().to_lowercase();
36
37        if response != "y" && response != "yes" {
38            println!("Installation cancelled.");
39            return Ok(());
40        }
41        println!();
42    }
43
44    // Fetch latest release info
45    println!("Fetching latest release information...");
46
47    const REPO: &str = "paulrobello/par-term";
48    let api_url = format!("https://api.github.com/repos/{}/releases/latest", REPO);
49    let (zip_url, checksum_url) = shader_installer::get_shaders_download_url(&api_url, REPO)
50        .map_err(|e| anyhow::anyhow!(e))?;
51
52    println!("Downloading shaders from: {}", zip_url);
53    println!();
54
55    // Download the zip file (with optional checksum verification)
56    let zip_data = shader_installer::download_and_verify(&zip_url, checksum_url.as_deref())
57        .map_err(|e| anyhow::anyhow!(e))?;
58
59    // Create shaders directory if it doesn't exist
60    std::fs::create_dir_all(&shaders_dir)?;
61
62    // Extract shaders
63    println!("Extracting shaders to {}...", shaders_dir.display());
64    shader_installer::extract_shaders(&zip_data, &shaders_dir).map_err(|e| anyhow::anyhow!(e))?;
65
66    // Count installed shaders
67    let shader_count = shader_installer::count_shader_files(&shaders_dir);
68
69    println!();
70    println!("=============================================");
71    println!("  Installation complete!");
72    println!("=============================================");
73    println!();
74    println!("Installed {} shaders to:", shader_count);
75    println!("  {}", shaders_dir.display());
76    println!();
77    println!("To use a shader, add to your config.yaml:");
78    println!("  custom_shader: \"shader_name.glsl\"");
79    println!("  custom_shader_enabled: true");
80    println!();
81    println!("For cursor shaders:");
82    println!("  cursor_shader: \"cursor_glow.glsl\"");
83    println!("  cursor_shader_enabled: true");
84    println!();
85    println!("See docs/SHADERS.md for the full shader gallery.");
86
87    Ok(())
88}
89
90/// Install shell integration for the specified or detected shell (CLI version)
91pub fn install_shell_integration_cli(shell: Option<ShellType>) -> anyhow::Result<()> {
92    let detected = shell_integration_installer::detected_shell();
93    let target_shell = shell.unwrap_or(detected);
94
95    println!("=============================================");
96    println!("  par-term Shell Integration Installer");
97    println!("=============================================");
98    println!();
99
100    if target_shell == ShellType::Unknown {
101        eprintln!("Error: Could not detect shell type.");
102        eprintln!("Please specify your shell with --shell bash|zsh|fish");
103        return Err(anyhow::anyhow!("Unknown shell type"));
104    }
105
106    println!("Detected shell: {:?}", target_shell);
107
108    // Check if already installed
109    if shell_integration_installer::is_installed() {
110        println!("Shell integration is already installed.");
111        print!("Do you want to reinstall? [y/N] ");
112        io::stdout().flush()?;
113
114        let mut response = String::new();
115        io::stdin().read_line(&mut response)?;
116        let response = response.trim().to_lowercase();
117
118        if response != "y" && response != "yes" {
119            println!("Installation cancelled.");
120            return Ok(());
121        }
122        println!();
123    }
124
125    println!("Installing shell integration...");
126
127    match shell_integration_installer::install(Some(target_shell)) {
128        Ok(result) => {
129            println!();
130            println!("=============================================");
131            println!("  Installation complete!");
132            println!("=============================================");
133            println!();
134            println!("Script installed to:");
135            println!("  {}", result.script_path.display());
136            println!();
137            println!("Added source line to:");
138            println!("  {}", result.rc_file.display());
139            println!();
140            if result.needs_restart {
141                println!("Please restart your shell or run:");
142                println!("  source {}", result.rc_file.display());
143            }
144            Ok(())
145        }
146        Err(e) => {
147            eprintln!("Error: {}", e);
148            Err(anyhow::anyhow!(e))
149        }
150    }
151}
152
153/// Uninstall shell integration (CLI version)
154pub fn uninstall_shell_integration_cli() -> anyhow::Result<()> {
155    println!("=============================================");
156    println!("  par-term Shell Integration Uninstaller");
157    println!("=============================================");
158    println!();
159
160    if !shell_integration_installer::is_installed() {
161        println!("Shell integration is not installed.");
162        return Ok(());
163    }
164
165    println!("Uninstalling shell integration...");
166
167    match shell_integration_installer::uninstall() {
168        Ok(result) => {
169            println!();
170            println!("=============================================");
171            println!("  Uninstallation complete!");
172            println!("=============================================");
173            println!();
174
175            if !result.cleaned.is_empty() {
176                println!("Cleaned RC files:");
177                for path in &result.cleaned {
178                    println!("  {}", path.display());
179                }
180                println!();
181            }
182
183            if !result.scripts_removed.is_empty() {
184                println!("Removed integration scripts:");
185                for path in &result.scripts_removed {
186                    println!("  {}", path.display());
187                }
188                println!();
189            }
190
191            if !result.needs_manual.is_empty() {
192                println!("WARNING: Some files need manual cleanup:");
193                for path in &result.needs_manual {
194                    println!("  {}", path.display());
195                }
196                println!();
197            }
198
199            Ok(())
200        }
201        Err(e) => {
202            eprintln!("Error: {}", e);
203            Err(anyhow::anyhow!(e))
204        }
205    }
206}
207
208/// Uninstall shaders using manifest (CLI version)
209pub fn uninstall_shaders_cli(force: bool) -> anyhow::Result<()> {
210    let shaders_dir = crate::config::Config::shaders_dir();
211
212    println!("=============================================");
213    println!("  par-term Shader Uninstaller");
214    println!("=============================================");
215    println!();
216    println!("Shaders directory: {}", shaders_dir.display());
217    println!();
218
219    if !shaders_dir.exists() {
220        println!("No shaders installed.");
221        return Ok(());
222    }
223
224    // Check for manifest
225    let manifest_path = shaders_dir.join("manifest.json");
226    if !manifest_path.exists() {
227        println!("No manifest.json found. Cannot determine which files are bundled.");
228        println!("Only files installed with the installer can be safely uninstalled.");
229        return Err(anyhow::anyhow!("No manifest found"));
230    }
231
232    if !force {
233        println!("This will remove bundled shader files.");
234        println!("User-created and modified files will be preserved.");
235        println!();
236        print!("Do you want to continue? [y/N] ");
237        io::stdout().flush()?;
238
239        let mut response = String::new();
240        io::stdin().read_line(&mut response)?;
241        let response = response.trim().to_lowercase();
242
243        if response != "y" && response != "yes" {
244            println!("Uninstallation cancelled.");
245            return Ok(());
246        }
247        println!();
248    }
249
250    println!("Uninstalling shaders...");
251
252    match shader_installer::uninstall_shaders(force) {
253        Ok(result) => {
254            println!();
255            println!("=============================================");
256            println!("  Uninstallation complete!");
257            println!("=============================================");
258            println!();
259            println!("Removed {} bundled files.", result.removed);
260
261            if result.kept > 0 {
262                println!("Preserved {} user files.", result.kept);
263            }
264
265            if !result.needs_confirmation.is_empty() {
266                println!();
267                println!("Modified files that were preserved:");
268                for path in &result.needs_confirmation {
269                    println!("  {}", path);
270                }
271            }
272
273            Ok(())
274        }
275        Err(e) => {
276            eprintln!("Error: {}", e);
277            Err(anyhow::anyhow!(e))
278        }
279    }
280}
281
282/// Self-update par-term to the latest version (CLI version)
283pub fn self_update_cli(skip_prompt: bool) -> anyhow::Result<()> {
284    use par_term_update::self_updater;
285    use par_term_update::update_checker;
286
287    println!("=============================================");
288    println!("  par-term Self-Updater");
289    println!("=============================================");
290    println!();
291
292    let current_version = env!("CARGO_PKG_VERSION");
293    println!("Current version: {}", current_version);
294
295    // Detect installation type
296    let installation = self_updater::detect_installation().map_err(|e| anyhow::anyhow!(e))?;
297    println!("Installation type: {}", installation.description());
298    println!();
299
300    // Check for managed installations early
301    match &installation {
302        self_updater::InstallationType::Homebrew => {
303            println!("par-term is installed via Homebrew.");
304            println!("Please update with:");
305            println!("  brew upgrade --cask par-term");
306            return Err(anyhow::anyhow!("Cannot self-update Homebrew installation"));
307        }
308        self_updater::InstallationType::CargoInstall => {
309            println!("par-term is installed via cargo.");
310            println!("Please update with:");
311            println!("  cargo install par-term");
312            return Err(anyhow::anyhow!("Cannot self-update cargo installation"));
313        }
314        _ => {}
315    }
316
317    // Check for updates
318    println!("Checking for updates...");
319    let release_info = update_checker::fetch_latest_release().map_err(|e| anyhow::anyhow!(e))?;
320
321    let latest_version = release_info
322        .version
323        .strip_prefix('v')
324        .unwrap_or(&release_info.version);
325
326    let current = semver::Version::parse(current_version)?;
327    let latest = semver::Version::parse(latest_version)?;
328
329    if latest <= current {
330        println!();
331        println!(
332            "You are already running the latest version ({}).",
333            current_version
334        );
335        return Ok(());
336    }
337
338    println!();
339    println!(
340        "New version available: {} -> {}",
341        current_version, latest_version
342    );
343    if let Some(ref notes) = release_info.release_notes {
344        println!();
345        println!("Release notes:");
346        // Show first few lines of release notes
347        for line in notes.lines().take(10) {
348            println!("  {}", line);
349        }
350        if notes.lines().count() > 10 {
351            println!("  ...");
352        }
353    }
354    println!();
355
356    // Confirm unless --yes
357    if !skip_prompt {
358        print!("Do you want to update? [y/N] ");
359        io::stdout().flush()?;
360
361        let mut response = String::new();
362        io::stdin().read_line(&mut response)?;
363        let response = response.trim().to_lowercase();
364
365        if response != "y" && response != "yes" {
366            println!("Update cancelled.");
367            return Ok(());
368        }
369        println!();
370    }
371
372    println!("Downloading and installing update...");
373
374    match self_updater::perform_update(latest_version, crate::VERSION) {
375        Ok(result) => {
376            println!();
377            println!("=============================================");
378            println!("  Update complete!");
379            println!("=============================================");
380            println!();
381            println!("Updated: {} -> {}", result.old_version, result.new_version);
382            println!("Location: {}", result.install_path.display());
383            if result.needs_restart {
384                println!();
385                println!("Please restart par-term to use the new version.");
386            }
387            Ok(())
388        }
389        Err(e) => {
390            eprintln!("Update failed: {}", e);
391            Err(anyhow::anyhow!(e))
392        }
393    }
394}
395
396/// Install both shaders and shell integration (CLI version)
397pub fn install_integrations_cli(skip_prompt: bool) -> anyhow::Result<()> {
398    println!("=============================================");
399    println!("  par-term Integrations Installer");
400    println!("=============================================");
401    println!();
402    println!("This will install:");
403    println!("  1. Shader collection from latest release");
404    println!("  2. Shell integration for your current shell");
405    println!();
406
407    if !skip_prompt {
408        print!("Do you want to continue? [y/N] ");
409        io::stdout().flush()?;
410
411        let mut response = String::new();
412        io::stdin().read_line(&mut response)?;
413        let response = response.trim().to_lowercase();
414
415        if response != "y" && response != "yes" {
416            println!("Installation cancelled.");
417            return Ok(());
418        }
419        println!();
420    }
421
422    // Install shaders
423    println!("Step 1: Installing shaders...");
424    println!("---------------------------------------------");
425
426    let shader_result = install_shaders_cli(true);
427    if shader_result.is_err() {
428        println!();
429        println!("WARNING: Shader installation failed.");
430        println!("Continuing with shell integration...");
431    }
432
433    println!();
434    println!("Step 2: Installing shell integration...");
435    println!("---------------------------------------------");
436
437    let shell_result = install_shell_integration_cli(None);
438
439    println!();
440    println!("=============================================");
441    println!("  Integrations Installation Summary");
442    println!("=============================================");
443    println!();
444
445    match (&shader_result, &shell_result) {
446        (Ok(()), Ok(())) => {
447            println!("All integrations installed successfully!");
448        }
449        (Err(_), Ok(())) => {
450            println!("Shell integration: INSTALLED");
451            println!("Shaders: FAILED (see above for errors)");
452        }
453        (Ok(()), Err(_)) => {
454            println!("Shaders: INSTALLED");
455            println!("Shell integration: FAILED (see above for errors)");
456        }
457        (Err(_), Err(_)) => {
458            println!("Both installations failed. See above for errors.");
459        }
460    }
461
462    // Return success if at least one succeeded
463    if shader_result.is_ok() || shell_result.is_ok() {
464        Ok(())
465    } else {
466        Err(anyhow::anyhow!("Both installations failed"))
467    }
468}