1use crate::config::ShellType;
6use crate::shader_installer;
7use crate::shell_integration_installer;
8use clap::{Parser, Subcommand};
9use std::io::{self, Write};
10use std::path::PathBuf;
11
12#[derive(Debug, Clone, Copy, clap::ValueEnum)]
14pub enum ShellTypeArg {
15 Bash,
16 Zsh,
17 Fish,
18}
19
20impl From<ShellTypeArg> for ShellType {
21 fn from(arg: ShellTypeArg) -> Self {
22 match arg {
23 ShellTypeArg::Bash => ShellType::Bash,
24 ShellTypeArg::Zsh => ShellType::Zsh,
25 ShellTypeArg::Fish => ShellType::Fish,
26 }
27 }
28}
29
30#[derive(Parser)]
32#[command(name = "par-term")]
33#[command(author, version, about, long_about = None)]
34pub struct Cli {
35 #[command(subcommand)]
36 pub command: Option<Commands>,
37
38 #[arg(long, value_name = "SHADER")]
40 pub shader: Option<String>,
41
42 #[arg(long, value_name = "SECONDS")]
44 pub exit_after: Option<f64>,
45
46 #[arg(long, value_name = "PATH", num_args = 0..=1, default_missing_value = "")]
48 pub screenshot: Option<PathBuf>,
49
50 #[arg(long, value_name = "COMMAND")]
52 pub command_to_send: Option<String>,
53
54 #[arg(long)]
56 pub log_session: bool,
57
58 #[arg(long, value_enum, value_name = "LEVEL")]
60 pub log_level: Option<LogLevelArg>,
61}
62
63#[derive(Debug, Clone, Copy, clap::ValueEnum)]
65pub enum LogLevelArg {
66 Off,
67 Error,
68 Warn,
69 Info,
70 Debug,
71 Trace,
72}
73
74impl LogLevelArg {
75 pub fn to_level_filter(self) -> log::LevelFilter {
77 match self {
78 LogLevelArg::Off => log::LevelFilter::Off,
79 LogLevelArg::Error => log::LevelFilter::Error,
80 LogLevelArg::Warn => log::LevelFilter::Warn,
81 LogLevelArg::Info => log::LevelFilter::Info,
82 LogLevelArg::Debug => log::LevelFilter::Debug,
83 LogLevelArg::Trace => log::LevelFilter::Trace,
84 }
85 }
86}
87
88#[derive(Subcommand)]
89pub enum Commands {
90 InstallShaders {
92 #[arg(short = 'y', long)]
94 yes: bool,
95
96 #[arg(short, long)]
98 force: bool,
99 },
100
101 InstallShellIntegration {
103 #[arg(long, value_enum)]
105 shell: Option<ShellTypeArg>,
106 },
107
108 UninstallShellIntegration,
110
111 UninstallShaders {
113 #[arg(short, long)]
115 force: bool,
116 },
117
118 InstallIntegrations {
120 #[arg(short = 'y', long)]
122 yes: bool,
123 },
124
125 SelfUpdate {
127 #[arg(short = 'y', long)]
129 yes: bool,
130 },
131
132 McpServer,
134}
135
136#[derive(Clone, Debug, Default)]
138pub struct RuntimeOptions {
139 pub shader: Option<String>,
141 pub exit_after: Option<f64>,
143 pub screenshot: Option<PathBuf>,
145 pub command_to_send: Option<String>,
147 pub log_session: bool,
149 pub log_level: Option<log::LevelFilter>,
151}
152
153pub enum CliResult {
155 Continue(RuntimeOptions),
157 Exit(i32),
159}
160
161pub fn process_cli() -> CliResult {
163 let cli = Cli::parse();
164
165 match cli.command {
166 Some(Commands::InstallShaders { yes, force }) => {
167 let result = install_shaders_cli(yes || force);
168 CliResult::Exit(if result.is_ok() { 0 } else { 1 })
169 }
170 Some(Commands::InstallShellIntegration { shell }) => {
171 let result = install_shell_integration_cli(shell.map(Into::into));
172 CliResult::Exit(if result.is_ok() { 0 } else { 1 })
173 }
174 Some(Commands::UninstallShellIntegration) => {
175 let result = uninstall_shell_integration_cli();
176 CliResult::Exit(if result.is_ok() { 0 } else { 1 })
177 }
178 Some(Commands::UninstallShaders { force }) => {
179 let result = uninstall_shaders_cli(force);
180 CliResult::Exit(if result.is_ok() { 0 } else { 1 })
181 }
182 Some(Commands::InstallIntegrations { yes }) => {
183 let result = install_integrations_cli(yes);
184 CliResult::Exit(if result.is_ok() { 0 } else { 1 })
185 }
186 Some(Commands::SelfUpdate { yes }) => {
187 let result = self_update_cli(yes);
188 CliResult::Exit(if result.is_ok() { 0 } else { 1 })
189 }
190 Some(Commands::McpServer) => {
191 crate::mcp_server::run_mcp_server();
192 }
193 None => {
194 let options = RuntimeOptions {
196 shader: cli.shader,
197 exit_after: cli.exit_after,
198 screenshot: cli.screenshot,
199 command_to_send: cli.command_to_send,
200 log_session: cli.log_session,
201 log_level: cli.log_level.map(|l| l.to_level_filter()),
202 };
203 CliResult::Continue(options)
204 }
205 }
206}
207
208fn install_shaders_cli(skip_prompt: bool) -> anyhow::Result<()> {
210 let shaders_dir = crate::config::Config::shaders_dir();
211
212 println!("=============================================");
213 println!(" par-term Shader Installer");
214 println!("=============================================");
215 println!();
216 println!("Target directory: {}", shaders_dir.display());
217 println!();
218
219 if shaders_dir.exists() && shader_installer::has_shader_files(&shaders_dir) && !skip_prompt {
221 println!("WARNING: This will overwrite existing shaders in:");
222 println!(" {}", shaders_dir.display());
223 println!();
224 print!("Do you want to continue? [y/N] ");
225 io::stdout().flush()?;
226
227 let mut response = String::new();
228 io::stdin().read_line(&mut response)?;
229 let response = response.trim().to_lowercase();
230
231 if response != "y" && response != "yes" {
232 println!("Installation cancelled.");
233 return Ok(());
234 }
235 println!();
236 }
237
238 println!("Fetching latest release information...");
240
241 const REPO: &str = "paulrobello/par-term";
242 let api_url = format!("https://api.github.com/repos/{}/releases/latest", REPO);
243 let download_url = shader_installer::get_shaders_download_url(&api_url, REPO)
244 .map_err(|e| anyhow::anyhow!(e))?;
245
246 println!("Downloading shaders from: {}", download_url);
247 println!();
248
249 let zip_data =
251 shader_installer::download_file(&download_url).map_err(|e| anyhow::anyhow!(e))?;
252
253 std::fs::create_dir_all(&shaders_dir)?;
255
256 println!("Extracting shaders to {}...", shaders_dir.display());
258 shader_installer::extract_shaders(&zip_data, &shaders_dir).map_err(|e| anyhow::anyhow!(e))?;
259
260 let shader_count = shader_installer::count_shader_files(&shaders_dir);
262
263 println!();
264 println!("=============================================");
265 println!(" Installation complete!");
266 println!("=============================================");
267 println!();
268 println!("Installed {} shaders to:", shader_count);
269 println!(" {}", shaders_dir.display());
270 println!();
271 println!("To use a shader, add to your config.yaml:");
272 println!(" custom_shader: \"shader_name.glsl\"");
273 println!(" custom_shader_enabled: true");
274 println!();
275 println!("For cursor shaders:");
276 println!(" cursor_shader: \"cursor_glow.glsl\"");
277 println!(" cursor_shader_enabled: true");
278 println!();
279 println!("See docs/SHADERS.md for the full shader gallery.");
280
281 Ok(())
282}
283
284fn install_shell_integration_cli(shell: Option<ShellType>) -> anyhow::Result<()> {
286 let detected = shell_integration_installer::detected_shell();
287 let target_shell = shell.unwrap_or(detected);
288
289 println!("=============================================");
290 println!(" par-term Shell Integration Installer");
291 println!("=============================================");
292 println!();
293
294 if target_shell == ShellType::Unknown {
295 eprintln!("Error: Could not detect shell type.");
296 eprintln!("Please specify your shell with --shell bash|zsh|fish");
297 return Err(anyhow::anyhow!("Unknown shell type"));
298 }
299
300 println!("Detected shell: {:?}", target_shell);
301
302 if shell_integration_installer::is_installed() {
304 println!("Shell integration is already installed.");
305 print!("Do you want to reinstall? [y/N] ");
306 io::stdout().flush()?;
307
308 let mut response = String::new();
309 io::stdin().read_line(&mut response)?;
310 let response = response.trim().to_lowercase();
311
312 if response != "y" && response != "yes" {
313 println!("Installation cancelled.");
314 return Ok(());
315 }
316 println!();
317 }
318
319 println!("Installing shell integration...");
320
321 match shell_integration_installer::install(Some(target_shell)) {
322 Ok(result) => {
323 println!();
324 println!("=============================================");
325 println!(" Installation complete!");
326 println!("=============================================");
327 println!();
328 println!("Script installed to:");
329 println!(" {}", result.script_path.display());
330 println!();
331 println!("Added source line to:");
332 println!(" {}", result.rc_file.display());
333 println!();
334 if result.needs_restart {
335 println!("Please restart your shell or run:");
336 println!(" source {}", result.rc_file.display());
337 }
338 Ok(())
339 }
340 Err(e) => {
341 eprintln!("Error: {}", e);
342 Err(anyhow::anyhow!(e))
343 }
344 }
345}
346
347fn uninstall_shell_integration_cli() -> anyhow::Result<()> {
349 println!("=============================================");
350 println!(" par-term Shell Integration Uninstaller");
351 println!("=============================================");
352 println!();
353
354 if !shell_integration_installer::is_installed() {
355 println!("Shell integration is not installed.");
356 return Ok(());
357 }
358
359 println!("Uninstalling shell integration...");
360
361 match shell_integration_installer::uninstall() {
362 Ok(result) => {
363 println!();
364 println!("=============================================");
365 println!(" Uninstallation complete!");
366 println!("=============================================");
367 println!();
368
369 if !result.cleaned.is_empty() {
370 println!("Cleaned RC files:");
371 for path in &result.cleaned {
372 println!(" {}", path.display());
373 }
374 println!();
375 }
376
377 if !result.scripts_removed.is_empty() {
378 println!("Removed integration scripts:");
379 for path in &result.scripts_removed {
380 println!(" {}", path.display());
381 }
382 println!();
383 }
384
385 if !result.needs_manual.is_empty() {
386 println!("WARNING: Some files need manual cleanup:");
387 for path in &result.needs_manual {
388 println!(" {}", path.display());
389 }
390 println!();
391 }
392
393 Ok(())
394 }
395 Err(e) => {
396 eprintln!("Error: {}", e);
397 Err(anyhow::anyhow!(e))
398 }
399 }
400}
401
402fn uninstall_shaders_cli(force: bool) -> anyhow::Result<()> {
404 let shaders_dir = crate::config::Config::shaders_dir();
405
406 println!("=============================================");
407 println!(" par-term Shader Uninstaller");
408 println!("=============================================");
409 println!();
410 println!("Shaders directory: {}", shaders_dir.display());
411 println!();
412
413 if !shaders_dir.exists() {
414 println!("No shaders installed.");
415 return Ok(());
416 }
417
418 let manifest_path = shaders_dir.join("manifest.json");
420 if !manifest_path.exists() {
421 println!("No manifest.json found. Cannot determine which files are bundled.");
422 println!("Only files installed with the installer can be safely uninstalled.");
423 return Err(anyhow::anyhow!("No manifest found"));
424 }
425
426 if !force {
427 println!("This will remove bundled shader files.");
428 println!("User-created and modified files will be preserved.");
429 println!();
430 print!("Do you want to continue? [y/N] ");
431 io::stdout().flush()?;
432
433 let mut response = String::new();
434 io::stdin().read_line(&mut response)?;
435 let response = response.trim().to_lowercase();
436
437 if response != "y" && response != "yes" {
438 println!("Uninstallation cancelled.");
439 return Ok(());
440 }
441 println!();
442 }
443
444 println!("Uninstalling shaders...");
445
446 match shader_installer::uninstall_shaders(force) {
447 Ok(result) => {
448 println!();
449 println!("=============================================");
450 println!(" Uninstallation complete!");
451 println!("=============================================");
452 println!();
453 println!("Removed {} bundled files.", result.removed);
454
455 if result.kept > 0 {
456 println!("Preserved {} user files.", result.kept);
457 }
458
459 if !result.needs_confirmation.is_empty() {
460 println!();
461 println!("Modified files that were preserved:");
462 for path in &result.needs_confirmation {
463 println!(" {}", path);
464 }
465 }
466
467 Ok(())
468 }
469 Err(e) => {
470 eprintln!("Error: {}", e);
471 Err(anyhow::anyhow!(e))
472 }
473 }
474}
475
476fn self_update_cli(skip_prompt: bool) -> anyhow::Result<()> {
478 use crate::self_updater;
479 use crate::update_checker;
480
481 println!("=============================================");
482 println!(" par-term Self-Updater");
483 println!("=============================================");
484 println!();
485
486 let current_version = env!("CARGO_PKG_VERSION");
487 println!("Current version: {}", current_version);
488
489 let installation = self_updater::detect_installation();
491 println!("Installation type: {}", installation.description());
492 println!();
493
494 match &installation {
496 self_updater::InstallationType::Homebrew => {
497 println!("par-term is installed via Homebrew.");
498 println!("Please update with:");
499 println!(" brew upgrade --cask par-term");
500 return Err(anyhow::anyhow!("Cannot self-update Homebrew installation"));
501 }
502 self_updater::InstallationType::CargoInstall => {
503 println!("par-term is installed via cargo.");
504 println!("Please update with:");
505 println!(" cargo install par-term");
506 return Err(anyhow::anyhow!("Cannot self-update cargo installation"));
507 }
508 _ => {}
509 }
510
511 println!("Checking for updates...");
513 let release_info = update_checker::fetch_latest_release().map_err(|e| anyhow::anyhow!(e))?;
514
515 let latest_version = release_info
516 .version
517 .strip_prefix('v')
518 .unwrap_or(&release_info.version);
519
520 let current = semver::Version::parse(current_version)?;
521 let latest = semver::Version::parse(latest_version)?;
522
523 if latest <= current {
524 println!();
525 println!(
526 "You are already running the latest version ({}).",
527 current_version
528 );
529 return Ok(());
530 }
531
532 println!();
533 println!(
534 "New version available: {} -> {}",
535 current_version, latest_version
536 );
537 if let Some(ref notes) = release_info.release_notes {
538 println!();
539 println!("Release notes:");
540 for line in notes.lines().take(10) {
542 println!(" {}", line);
543 }
544 if notes.lines().count() > 10 {
545 println!(" ...");
546 }
547 }
548 println!();
549
550 if !skip_prompt {
552 print!("Do you want to update? [y/N] ");
553 io::stdout().flush()?;
554
555 let mut response = String::new();
556 io::stdin().read_line(&mut response)?;
557 let response = response.trim().to_lowercase();
558
559 if response != "y" && response != "yes" {
560 println!("Update cancelled.");
561 return Ok(());
562 }
563 println!();
564 }
565
566 println!("Downloading and installing update...");
567
568 match self_updater::perform_update(latest_version) {
569 Ok(result) => {
570 println!();
571 println!("=============================================");
572 println!(" Update complete!");
573 println!("=============================================");
574 println!();
575 println!("Updated: {} -> {}", result.old_version, result.new_version);
576 println!("Location: {}", result.install_path.display());
577 if result.needs_restart {
578 println!();
579 println!("Please restart par-term to use the new version.");
580 }
581 Ok(())
582 }
583 Err(e) => {
584 eprintln!("Update failed: {}", e);
585 Err(anyhow::anyhow!(e))
586 }
587 }
588}
589
590fn install_integrations_cli(skip_prompt: bool) -> anyhow::Result<()> {
592 println!("=============================================");
593 println!(" par-term Integrations Installer");
594 println!("=============================================");
595 println!();
596 println!("This will install:");
597 println!(" 1. Shader collection from latest release");
598 println!(" 2. Shell integration for your current shell");
599 println!();
600
601 if !skip_prompt {
602 print!("Do you want to continue? [y/N] ");
603 io::stdout().flush()?;
604
605 let mut response = String::new();
606 io::stdin().read_line(&mut response)?;
607 let response = response.trim().to_lowercase();
608
609 if response != "y" && response != "yes" {
610 println!("Installation cancelled.");
611 return Ok(());
612 }
613 println!();
614 }
615
616 println!("Step 1: Installing shaders...");
618 println!("---------------------------------------------");
619
620 let shader_result = install_shaders_cli(true);
621 if shader_result.is_err() {
622 println!();
623 println!("WARNING: Shader installation failed.");
624 println!("Continuing with shell integration...");
625 }
626
627 println!();
628 println!("Step 2: Installing shell integration...");
629 println!("---------------------------------------------");
630
631 let shell_result = install_shell_integration_cli(None);
632
633 println!();
634 println!("=============================================");
635 println!(" Integrations Installation Summary");
636 println!("=============================================");
637 println!();
638
639 match (&shader_result, &shell_result) {
640 (Ok(()), Ok(())) => {
641 println!("All integrations installed successfully!");
642 }
643 (Err(_), Ok(())) => {
644 println!("Shell integration: INSTALLED");
645 println!("Shaders: FAILED (see above for errors)");
646 }
647 (Ok(()), Err(_)) => {
648 println!("Shaders: INSTALLED");
649 println!("Shell integration: FAILED (see above for errors)");
650 }
651 (Err(_), Err(_)) => {
652 println!("Both installations failed. See above for errors.");
653 }
654 }
655
656 if shader_result.is_ok() || shell_result.is_ok() {
658 Ok(())
659 } else {
660 Err(anyhow::anyhow!("Both installations failed"))
661 }
662}