1use std::io::{self, Write};
9
10use crate::config::ShellType;
11use crate::shader_installer;
12use crate::shell_integration_installer;
13
14pub 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 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 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 let zip_data = shader_installer::download_and_verify(&zip_url, checksum_url.as_deref())
57 .map_err(|e| anyhow::anyhow!(e))?;
58
59 std::fs::create_dir_all(&shaders_dir)?;
61
62 println!("Extracting shaders to {}...", shaders_dir.display());
64 shader_installer::extract_shaders(&zip_data, &shaders_dir).map_err(|e| anyhow::anyhow!(e))?;
65
66 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
90pub 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 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
153pub 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
208pub 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 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
282pub 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 let installation = self_updater::detect_installation().map_err(|e| anyhow::anyhow!(e))?;
297 println!("Installation type: {}", installation.description());
298 println!();
299
300 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 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 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 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
396pub 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 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 if shader_result.is_ok() || shell_result.is_ok() {
464 Ok(())
465 } else {
466 Err(anyhow::anyhow!("Both installations failed"))
467 }
468}
469
470pub fn install_mux_extensions_cli(skip_prompt: bool) -> anyhow::Result<()> {
472 use crate::mux_extension_installer;
473
474 println!("=============================================");
475 println!(" par-term par-mux Extension Installer");
476 println!("=============================================");
477 println!();
478 println!("This installs the par-mux agent-state extension into each");
479 println!("installed agent's extension directory:");
480 println!(
481 " pi: {}",
482 dir_display(mux_extension_installer::pi_extension_dir())
483 );
484 println!(
485 " omp: {}",
486 dir_display(mux_extension_installer::omp_extension_dir())
487 );
488 println!();
489 println!("An agent that is not installed is reported and skipped.");
490
491 if !skip_prompt {
492 println!();
493 print!("Do you want to continue? [y/N] ");
494 io::stdout().flush()?;
495
496 let mut response = String::new();
497 io::stdin().read_line(&mut response)?;
498 let response = response.trim().to_lowercase();
499
500 if response != "y" && response != "yes" {
501 println!("Installation cancelled.");
502 return Ok(());
503 }
504 }
505 println!();
506
507 let pi_result = mux_extension_installer::install_pi_extension();
508 report_install("pi", &pi_result);
509 let omp_result = mux_extension_installer::install_omp_extension();
510 report_install("omp", &omp_result);
511
512 if pi_result.is_ok() || omp_result.is_ok() {
513 println!();
514 println!("At least one extension installed successfully.");
515 Ok(())
516 } else {
517 println!();
518 Err(anyhow::anyhow!("No extension installed (see errors above)"))
519 }
520}
521
522pub fn uninstall_mux_extensions_cli() -> anyhow::Result<()> {
524 use crate::mux_extension_installer;
525
526 println!("=============================================");
527 println!(" par-term par-mux Extension Uninstaller");
528 println!("=============================================");
529 println!();
530
531 let pi_result = mux_extension_installer::uninstall_pi_extension();
532 report_removal("pi", &pi_result);
533 let omp_result = mux_extension_installer::uninstall_omp_extension();
534 report_removal("omp", &omp_result);
535
536 println!();
537 println!("Done.");
538 Ok(())
539}
540
541pub fn install_mux_hooks_cli(skip_prompt: bool) -> anyhow::Result<()> {
543 use crate::mux_hook_installer;
544
545 println!("=============================================");
546 println!(" par-term par-mux Hook Installer");
547 println!("=============================================");
548 println!();
549 println!("This installs the par-mux session hooks. Unlike the pi/omp");
550 println!("extensions (a file drop into the agent's directory), these agents");
551 println!("have no extension directory: the hook is an ENTRY inside your");
552 println!("existing agent config, merged without touching your other settings,");
553 println!("comments, or formatting. Reinstalling is a no-op; uninstalling");
554 println!("removes only par-term's entries.");
555 println!();
556 println!("Agents:");
557 println!(
558 " claude: {}",
559 dir_display(mux_hook_installer::claude_settings_path())
560 );
561 println!(
562 " codex: {}",
563 dir_display(mux_hook_installer::codex_config_dir())
564 );
565 println!(
566 " grok: {}",
567 dir_display(mux_hook_installer::grok_config_dir().map(|dir| dir.join("hooks")))
568 );
569 println!();
570
571 if !skip_prompt {
572 println!();
573 print!("Do you want to continue? [y/N] ");
574 io::stdout().flush()?;
575
576 let mut response = String::new();
577 io::stdin().read_line(&mut response)?;
578 let response = response.trim().to_lowercase();
579
580 if response != "y" && response != "yes" {
581 println!("Installation cancelled.");
582 return Ok(());
583 }
584 }
585 println!();
586
587 let claude_result = mux_hook_installer::install_claude_hook();
588 match &claude_result {
589 Ok(result) if result.settings_changed => {
590 println!(
591 "claude: hook entry added to {} (script at {})",
592 result.settings_path.display(),
593 result.hook_path.display()
594 );
595 }
596 Ok(result) => {
597 println!(
598 "claude: already installed ({} unchanged, script refreshed)",
599 result.settings_path.display()
600 );
601 }
602 Err(err) => println!("claude: FAILED — {err}"),
603 }
604
605 let codex_result = mux_hook_installer::install_codex_hook();
608 match &codex_result {
609 Ok(result) => {
610 let summary = match (result.hooks_changed, result.config_changed) {
611 (true, true) => format!(
612 "hook entry added to {} and features.hooks enabled in {} (script at {})",
613 result.hooks_path.display(),
614 result.config_path.display(),
615 result.hook_path.display()
616 ),
617 (true, false) => format!(
618 "hook entry added to {} (features.hooks already enabled; script at {})",
619 result.hooks_path.display(),
620 result.hook_path.display()
621 ),
622 (false, true) => format!(
623 "features.hooks enabled in {} (hook entry already present; script at {})",
624 result.config_path.display(),
625 result.hook_path.display()
626 ),
627 (false, false) => format!(
628 "already installed ({} unchanged, script refreshed)",
629 result.hooks_path.display()
630 ),
631 };
632 println!("codex: {summary}");
633 }
634 Err(err) => println!("codex: FAILED — {err}"),
635 }
636
637 let grok_result = mux_hook_installer::install_grok_hook();
640 match &grok_result {
641 Ok(result) => println!(
642 "grok: hook config written to {} (script at {})",
643 result.config_path.display(),
644 result.hook_path.display()
645 ),
646 Err(err) => println!("grok: FAILED — {err}"),
647 }
648
649 if claude_result.is_ok() || codex_result.is_ok() || grok_result.is_ok() {
650 println!();
651 println!("At least one hook installed successfully.");
652 Ok(())
653 } else {
654 println!();
655 Err(anyhow::anyhow!("No hook installed (see errors above)"))
656 }
657}
658
659pub fn uninstall_mux_hooks_cli() -> anyhow::Result<()> {
661 use crate::mux_hook_installer;
662
663 println!("=============================================");
664 println!(" par-term par-mux Hook Uninstaller");
665 println!("=============================================");
666 println!();
667
668 let claude_result = mux_hook_installer::uninstall_claude_hook();
669 match &claude_result {
670 Ok(result) => {
671 if result.settings_changed {
672 println!(
673 "claude: entries removed from {}",
674 result.settings_path.display()
675 );
676 }
677 if result.hook_removed {
678 println!("claude: removed {}", result.hook_path.display());
679 }
680 if !result.settings_changed && !result.hook_removed {
681 if result.hook_path.is_file() {
682 println!(
683 "claude: {} exists but is not par-term's (left untouched)",
684 result.hook_path.display()
685 );
686 } else {
687 println!("claude: not installed");
688 }
689 }
690 }
691 Err(err) => println!("claude: FAILED — {err}"),
692 }
693
694 let codex_result = mux_hook_installer::uninstall_codex_hook();
695 match &codex_result {
696 Ok(result) => {
697 if result.hooks_changed {
698 println!(
699 "codex: entries removed from {}",
700 result.hooks_path.display()
701 );
702 }
703 if result.hook_removed {
704 println!("codex: removed {}", result.hook_path.display());
705 }
706 if !result.hooks_changed && !result.hook_removed {
707 if result.hook_path.is_file() {
708 println!(
709 "codex: {} exists but is not par-term's (left untouched)",
710 result.hook_path.display()
711 );
712 } else {
713 println!("codex: not installed");
714 }
715 }
716 if result.hooks_changed || result.hook_removed {
717 println!(
718 "codex: note: [features] hooks = true left in {} — inert while no hooks remain; remove it manually if unwanted",
719 result.config_path.display()
720 );
721 }
722 }
723 Err(err) => println!("codex: FAILED — {err}"),
724 }
725
726 let grok_result = mux_hook_installer::uninstall_grok_hook();
727 match &grok_result {
728 Ok(result) => {
729 if result.config_removed {
730 println!("grok: removed {}", result.config_path.display());
731 }
732 if result.hook_removed {
733 println!("grok: removed {}", result.hook_path.display());
734 }
735 if !result.config_removed && !result.hook_removed {
736 if result.hook_path.is_file() {
737 println!(
738 "grok: {} exists but is not par-term's (left untouched)",
739 result.hook_path.display()
740 );
741 } else {
742 println!("grok: not installed");
743 }
744 }
745 }
746 Err(err) => println!("grok: FAILED — {err}"),
747 }
748
749 println!();
750 println!("Done.");
751 Ok(())
752}
753
754fn dir_display(dir: std::io::Result<std::path::PathBuf>) -> String {
755 match dir {
756 Ok(path) => path.display().to_string(),
757 Err(err) => format!("<unresolved: {err}>"),
758 }
759}
760
761fn report_install(agent: &str, result: &std::io::Result<std::path::PathBuf>) {
762 match result {
763 Ok(path) => println!("{agent}: installed at {}", path.display()),
764 Err(err) => println!("{agent}: FAILED — {err}"),
765 }
766}
767
768fn report_removal(
769 agent: &str,
770 result: &std::io::Result<crate::mux_extension_installer::ExtensionRemoval>,
771) {
772 match result {
773 Ok(removal) if removal.removed => {
774 println!("{agent}: removed {}", removal.path.display());
775 }
776 Ok(removal) if removal.path.is_file() => println!(
777 "{agent}: {} exists but is not par-term's (left untouched)",
778 removal.path.display()
779 ),
780 Ok(_) => println!("{agent}: not installed"),
781 Err(err) => println!("{agent}: FAILED — {err}"),
782 }
783}