1#![allow(dead_code)]
2#![allow(clippy::missing_errors_doc)]
3#![allow(clippy::too_many_lines)]
4use std::collections::BTreeMap;
5use std::env;
6use std::fmt::Write;
7use std::fs;
8use std::process::Command;
9
10use anyhow::{Context, Result};
11use clap::{Args, Parser, Subcommand};
12use clap_complete::{ArgValueCandidates, CompletionCandidate};
13
14use pacs_core::{Pacs, PacsCommand, Scope};
15
16const BOLD: &str = "\x1b[1m";
17const GREEN: &str = "\x1b[32m";
18const BLUE: &str = "\x1b[34m";
19const YELLOW: &str = "\x1b[33m";
20const MAGENTA: &str = "\x1b[35m";
21const CYAN: &str = "\x1b[36m";
22const WHITE: &str = "\x1b[37m";
23const GREY: &str = "\x1b[90m";
24const RESET: &str = "\x1b[0m";
25
26#[derive(Parser, Debug)]
28#[command(name = "pacs")]
29#[command(author, version, about, long_about = None)]
30pub struct Cli {
31 #[command(subcommand)]
32 pub command: Commands,
33}
34
35#[derive(Subcommand, Debug)]
36pub enum Commands {
37 Init,
39
40 Add(AddArgs),
42
43 #[command(visible_alias = "rm")]
45 Remove(RemoveArgs),
46
47 Edit(EditArgs),
49
50 Rename(RenameArgs),
52
53 #[command(visible_alias = "ls")]
55 List(ListArgs),
56
57 Run(RunArgs),
59
60 #[command(visible_alias = "cp")]
62 Copy(CopyArgs),
63
64 Search(SearchArgs),
66
67 #[command(visible_alias = "p")]
69 Project {
70 #[command(subcommand)]
71 command: ProjectCommands,
72 },
73
74 #[command(visible_alias = "c")]
76 Context {
77 #[command(subcommand)]
78 command: ContextCommands,
79 },
80}
81
82#[derive(Subcommand, Debug)]
83pub enum ProjectCommands {
84 Add(ProjectAddArgs),
86
87 #[command(visible_alias = "rm")]
89 Remove(ProjectRemoveArgs),
90
91 #[command(visible_alias = "ls")]
93 List,
94
95 Activate(ProjectActivateArgs),
97
98 Deactivate,
100
101 Active,
103}
104
105#[derive(Subcommand, Debug)]
106pub enum ContextCommands {
107 Add(ContextAddArgs),
109
110 #[command(visible_alias = "rm")]
112 Remove(ContextRemoveArgs),
113
114 Edit(ContextEditArgs),
116
117 #[command(visible_alias = "ls")]
119 List(ContextListArgs),
120
121 Activate(ContextActivateArgs),
123
124 Active(ContextActiveArgs),
126}
127
128#[derive(Args, Debug)]
129pub struct ProjectAddArgs {
130 pub name: String,
132
133 #[arg(short, long)]
135 pub path: Option<String>,
136}
137
138#[derive(Args, Debug)]
139pub struct ProjectRemoveArgs {
140 #[arg(add = ArgValueCandidates::new(complete_projects))]
142 pub name: String,
143}
144
145#[derive(Args, Debug)]
146pub struct ProjectActivateArgs {
147 #[arg(add = ArgValueCandidates::new(complete_projects))]
149 pub name: String,
150}
151
152#[derive(Args, Debug)]
153pub struct ContextAddArgs {
154 pub name: String,
156
157 #[arg(short, long, add = ArgValueCandidates::new(complete_projects))]
159 pub project: Option<String>,
160}
161
162#[derive(Args, Debug)]
163pub struct ContextRemoveArgs {
164 pub name: String,
166
167 #[arg(short, long, add = ArgValueCandidates::new(complete_projects))]
169 pub project: Option<String>,
170}
171
172#[derive(Args, Debug)]
173pub struct ContextEditArgs {
174 #[arg(short, long, add = ArgValueCandidates::new(complete_projects))]
176 pub project: Option<String>,
177}
178
179#[derive(Args, Debug)]
180pub struct ContextListArgs {
181 #[arg(short, long, add = ArgValueCandidates::new(complete_projects))]
183 pub project: Option<String>,
184}
185
186#[derive(Args, Debug)]
187pub struct ContextActivateArgs {
188 pub name: String,
190
191 #[arg(short, long, add = ArgValueCandidates::new(complete_projects))]
193 pub project: Option<String>,
194}
195
196#[derive(Args, Debug)]
197pub struct ContextDeactivateArgs {
198 #[arg(short, long, add = ArgValueCandidates::new(complete_projects))]
200 pub project: Option<String>,
201}
202
203#[derive(Args, Debug)]
204pub struct ContextActiveArgs {
205 #[arg(short, long, add = ArgValueCandidates::new(complete_projects))]
207 pub project: Option<String>,
208}
209
210#[derive(Args, Debug)]
211pub struct AddArgs {
212 pub name: String,
214
215 pub command: Option<String>,
217
218 #[arg(short, long, add = ArgValueCandidates::new(complete_projects))]
220 pub project: Option<String>,
221
222 #[arg(short, long)]
224 pub global: bool,
225
226 #[arg(short, long)]
228 pub cwd: Option<String>,
229
230 #[arg(short, long, default_value = "", add = ArgValueCandidates::new(complete_tags))]
232 pub tag: String,
233}
234
235#[derive(Args, Debug)]
236pub struct CopyArgs {
237 #[arg(add = ArgValueCandidates::new(complete_commands))]
239 pub name: String,
240
241 #[arg(short = 'c', long = "context")]
243 pub context: Option<String>,
244}
245
246#[derive(Args, Debug)]
247pub struct SearchArgs {
248 pub query: String,
250}
251
252#[derive(Args, Debug)]
253pub struct RemoveArgs {
254 #[arg(add = ArgValueCandidates::new(complete_commands))]
256 pub name: String,
257}
258
259#[derive(Args, Debug)]
260pub struct EditArgs {
261 #[arg(add = ArgValueCandidates::new(complete_commands))]
263 pub name: String,
264}
265
266#[derive(Args, Debug)]
267pub struct RenameArgs {
268 #[arg(add = ArgValueCandidates::new(complete_commands))]
270 pub old_name: String,
271
272 pub new_name: String,
274}
275
276#[derive(Args, Debug)]
277pub struct ListArgs {
278 #[arg(add = ArgValueCandidates::new(complete_commands))]
280 pub name: Option<String>,
281
282 #[arg(short, long, add = ArgValueCandidates::new(complete_projects))]
284 pub project: Option<String>,
285
286 #[arg(short, long)]
288 pub global: bool,
289
290 #[arg(short, long, add = ArgValueCandidates::new(complete_tags))]
292 pub tag: Option<String>,
293
294 #[arg(short = 'c', long = "context")]
296 pub context: Option<String>,
297
298 #[arg(short, long)]
300 pub names: bool,
301}
302
303#[derive(Args, Debug)]
304pub struct RunArgs {
305 #[arg(add = ArgValueCandidates::new(complete_commands))]
307 pub name: String,
308
309 #[arg(short, long, add = ArgValueCandidates::new(complete_projects))]
311 pub project: Option<String>,
312
313 #[arg(short = 'c', long = "context")]
315 pub context: Option<String>,
316}
317
318fn complete_commands() -> Vec<CompletionCandidate> {
319 let Ok(pacs) = Pacs::init_home() else {
320 return vec![];
321 };
322 pacs.suggest_command_names()
323 .into_iter()
324 .map(CompletionCandidate::new)
325 .collect()
326}
327
328fn complete_projects() -> Vec<CompletionCandidate> {
329 let Ok(pacs) = Pacs::init_home() else {
330 return vec![];
331 };
332 pacs.suggest_projects()
333 .into_iter()
334 .map(CompletionCandidate::new)
335 .collect()
336}
337
338fn complete_tags() -> Vec<CompletionCandidate> {
339 let Ok(pacs) = Pacs::init_home() else {
340 return vec![];
341 };
342 pacs.suggest_tags()
343 .into_iter()
344 .map(CompletionCandidate::new)
345 .collect()
346}
347
348pub fn run(cli: Cli) -> Result<()> {
349 let mut pacs = Pacs::init_home().context("Failed to initialize pacs")?;
350
351 match cli.command {
352 Commands::Init => {
353 println!("Pacs initialized at ~/.pacs/");
354 }
355
356 Commands::Add(args) => {
357 let command = if let Some(cmd) = args.command {
358 cmd
359 } else {
360 let editor = env::var("VISUAL")
361 .ok()
362 .or_else(|| env::var("EDITOR").ok())
363 .unwrap_or_else(|| "vi".to_string());
364
365 let temp_file =
366 std::env::temp_dir().join(format!("pacs-{}.sh", std::process::id()));
367
368 fs::write(&temp_file, "")?;
369
370 let status = Command::new(&editor)
371 .arg(&temp_file)
372 .status()
373 .with_context(|| format!("Failed to open editor '{editor}'"))?;
374
375 if !status.success() {
376 fs::remove_file(&temp_file).ok();
377 anyhow::bail!("Editor exited with non-zero status");
378 }
379
380 let content = fs::read_to_string(&temp_file)?;
381 fs::remove_file(&temp_file).ok();
382
383 let command = content.trim().to_string();
384
385 if command.is_empty() {
386 anyhow::bail!("No command entered");
387 }
388
389 command + "\n"
390 };
391
392 let pacs_cmd = PacsCommand {
393 name: args.name.clone(),
394 command,
395 cwd: args.cwd,
396 tag: args.tag,
397 };
398
399 let scope_name: Option<String> = if let Some(ref p) = args.project {
401 Some(p.clone())
402 } else if args.global {
403 None
404 } else {
405 pacs.get_active_project()?
406 };
407
408 if let Some(ref project) = scope_name {
409 pacs.add_command(pacs_cmd, Scope::Project(project))
410 .with_context(|| format!("Failed to add command '{}'", args.name))?;
411 println!("Command '{}' added to project '{}'.", args.name, project);
412 } else {
413 pacs.add_command(pacs_cmd, Scope::Global)
414 .with_context(|| format!("Failed to add command '{}'", args.name))?;
415 println!("Command '{}' added to global.", args.name);
416 }
417 }
418
419 Commands::Remove(args) => {
420 pacs.delete_command_auto(&args.name)
421 .with_context(|| format!("Failed to remove command '{}'", args.name))?;
422 println!("Command '{}' removed.", args.name);
423 }
424
425 Commands::Edit(args) => {
426 let cmd = pacs
427 .get_command_auto(&args.name)
428 .with_context(|| format!("Command '{}' not found", args.name))?;
429
430 let editor = env::var("VISUAL")
431 .ok()
432 .or_else(|| env::var("EDITOR").ok())
433 .unwrap_or_else(|| "vi".to_string());
434
435 let temp_file =
436 std::env::temp_dir().join(format!("pacs-edit-{}.sh", std::process::id()));
437
438 fs::write(&temp_file, &cmd.command)?;
439
440 let status = Command::new(&editor)
441 .arg(&temp_file)
442 .status()
443 .with_context(|| format!("Failed to open editor '{editor}'"))?;
444
445 if !status.success() {
446 fs::remove_file(&temp_file).ok();
447 anyhow::bail!("Editor exited with non-zero status");
448 }
449
450 let new_command = fs::read_to_string(&temp_file)?;
451 fs::remove_file(&temp_file).ok();
452
453 if new_command.trim().is_empty() {
454 anyhow::bail!("Command cannot be empty");
455 }
456
457 pacs.update_command_auto(&args.name, new_command)
458 .with_context(|| format!("Failed to update command '{}'", args.name))?;
459 println!("Command '{}' updated.", args.name);
460 }
461
462 Commands::Rename(args) => {
463 pacs.rename_command_auto(&args.old_name, &args.new_name)
464 .with_context(|| {
465 format!(
466 "Failed to rename command '{}' to '{}'",
467 args.old_name, args.new_name
468 )
469 })?;
470 println!(
471 "Command '{}' renamed to '{}'.",
472 args.old_name, args.new_name
473 );
474 }
475
476 Commands::List(args) => {
477 if let Some(ref name) = args.name {
478 let cmd = pacs
479 .get_command_auto(name)
480 .with_context(|| format!("Command '{name}' not found"))?;
481 let tag_badge = if cmd.tag.is_empty() {
482 String::new()
483 } else {
484 format!(" {BOLD}{YELLOW}[{}]{RESET}", cmd.tag)
485 };
486 let cwd_badge = if let Some(ref cwd) = cmd.cwd {
487 format!(" {GREY}({cwd}){RESET}")
488 } else {
489 String::new()
490 };
491 println!("{BOLD}{CYAN}{}{RESET}{}{}", cmd.name, tag_badge, cwd_badge);
492 println!();
493 for line in cmd.command.lines() {
494 println!("{BLUE}{line}{RESET}");
495 }
496 return Ok(());
497 }
498
499 let filter_tag =
500 |cmd: &PacsCommand| -> bool { args.tag.as_ref().is_none_or(|t| &cmd.tag == t) };
501
502 let print_tagged = |commands: &[PacsCommand], scope_name: &str| {
503 if commands.is_empty() {
504 return;
505 }
506
507 let mut tags: BTreeMap<Option<&str>, Vec<&PacsCommand>> = BTreeMap::new();
508 for cmd in commands.iter().filter(|c| filter_tag(c)) {
509 let key = if cmd.tag.is_empty() {
510 None
511 } else {
512 Some(cmd.tag.as_str())
513 };
514 tags.entry(key).or_default().push(cmd);
515 }
516
517 if tags.is_empty() {
518 return;
519 }
520
521 println!("{BOLD}{MAGENTA}── {scope_name} ──{RESET}");
522
523 for (tag, cmds) in tags {
524 if let Some(name) = tag {
525 println!("{BOLD}{YELLOW}[{name}]{RESET}");
526 }
527
528 for cmd in cmds {
529 if args.names {
530 println!("{BOLD}{CYAN}{}{RESET}", cmd.name);
531 } else {
532 let cwd_badge = if let Some(ref cwd) = cmd.cwd {
533 format!(" {GREY}({cwd}){RESET}")
534 } else {
535 String::new()
536 };
537 println!("{BOLD}{CYAN}{}{RESET}{}", cmd.name, cwd_badge);
538 for line in cmd.command.lines() {
539 println!("{BLUE}{line}{RESET}");
540 }
541 println!();
542 }
543 }
544 }
545 };
546
547 if let Some(ref project) = args.project {
548 let commands =
549 pacs.list_commands(Scope::Project(project), args.context.as_deref())?;
550 print_tagged(&commands, project);
551 } else if args.global {
552 let commands = pacs.list_commands(Scope::Global, None)?;
553 print_tagged(&commands, "Global");
554 } else {
555 let commands = pacs.list_commands(Scope::Global, None)?;
556 print_tagged(&commands, "Global");
557
558 if let Some(active_project) = pacs.get_active_project()? {
559 let commands = pacs
560 .list_commands(Scope::Project(&active_project), args.context.as_deref())?;
561 print_tagged(&commands, &active_project);
562 } else {
563 for project in &pacs.projects {
564 let commands = pacs.list_commands(
565 Scope::Project(&project.name),
566 args.context.as_deref(),
567 )?;
568 print_tagged(&commands, &project.name);
569 }
570 }
571 }
572 }
573
574 Commands::Run(args) => {
575 match (&args.project, &args.context) {
577 (Some(project), Some(ctx)) => {
578 let prev_ctx = pacs.get_active_context(project).ok().flatten();
579 pacs.activate_context(project, ctx).ok();
580 let result = pacs.run(&args.name, Scope::Project(project));
581 match prev_ctx {
582 Some(prev) => {
583 pacs.activate_context(project, &prev).ok();
584 }
585 None => {
586 pacs.deactivate_context(project).ok();
587 }
588 }
589 result.with_context(|| format!("Failed to run command '{}'", args.name))?;
590 }
591 (Some(project), None) => {
592 pacs.run(&args.name, Scope::Project(project))
593 .with_context(|| format!("Failed to run command '{}'", args.name))?;
594 }
595 (None, Some(ctx)) => {
596 if let Some(active_project) = pacs.get_active_project()? {
598 let prev_ctx = pacs.get_active_context(&active_project).ok().flatten();
599 pacs.activate_context(&active_project, ctx).ok();
600 let result = pacs.run(&args.name, Scope::Project(&active_project));
601 match prev_ctx {
602 Some(prev) => {
603 pacs.activate_context(&active_project, &prev).ok();
604 }
605 None => {
606 pacs.deactivate_context(&active_project).ok();
607 }
608 }
609 result.with_context(|| format!("Failed to run command '{}'", args.name))?;
610 } else {
611 anyhow::bail!("No active project. Use '--project' with '--context'.");
612 }
613 }
614 (None, None) => {
615 pacs.run_auto(&args.name)
616 .with_context(|| format!("Failed to run command '{}'", args.name))?;
617 }
618 }
619 }
620
621 Commands::Copy(args) => {
622 let cmd = match (&args.context, pacs.get_active_project()?) {
623 (Some(ctx), Some(active_project)) => {
624 let prev_ctx = pacs.get_active_context(&active_project).ok().flatten();
626 pacs.activate_context(&active_project, ctx).ok();
627 let expanded = pacs
628 .expand_command_auto(&args.name)
629 .with_context(|| format!("Command '{}' not found", args.name));
630 match prev_ctx {
631 Some(prev) => {
632 pacs.activate_context(&active_project, &prev).ok();
633 }
634 None => {
635 pacs.deactivate_context(&active_project).ok();
636 }
637 }
638 expanded?
639 }
640 (Some(_), None) => {
641 anyhow::bail!("No active project. Use '--project' with '--context'.");
642 }
643 (None, _) => pacs
644 .expand_command_auto(&args.name)
645 .with_context(|| format!("Command '{}' not found", args.name))?,
646 };
647 arboard::Clipboard::new()
648 .and_then(|mut cb| cb.set_text(cmd.command.trim()))
649 .map_err(|e| anyhow::anyhow!("Failed to copy to clipboard: {e}"))?;
650 println!("Copied '{}' to clipboard.", args.name);
651 }
652
653 Commands::Search(args) => {
654 let matches = pacs.search(&args.query);
655 if matches.is_empty() {
656 println!("No matches found.");
657 } else {
658 for cmd in matches {
659 println!("{}", cmd.name);
660 }
661 }
662 }
663
664 Commands::Project { command } => match command {
665 ProjectCommands::Add(args) => {
666 pacs.init_project(&args.name, args.path)
667 .with_context(|| format!("Failed to create project '{}'", args.name))?;
668 println!("Project '{}' created.", args.name);
669 }
670 ProjectCommands::Remove(args) => {
671 pacs.delete_project(&args.name)
672 .with_context(|| format!("Failed to delete project '{}'", args.name))?;
673 println!("Project '{}' deleted.", args.name);
674 }
675 ProjectCommands::List => {
676 if pacs.projects.is_empty() {
677 println!("No projects.");
678 } else {
679 let active = pacs.get_active_project().ok().flatten();
680 for project in &pacs.projects {
681 let path_info = project
682 .path
683 .as_ref()
684 .map(|p| format!(" ({p})"))
685 .unwrap_or_default();
686 let active_marker = if active.as_ref() == Some(&project.name) {
687 format!(" {GREEN}*{RESET}")
688 } else {
689 String::new()
690 };
691 println!(
692 "{}{}{}{}{}",
693 BLUE, project.name, RESET, path_info, active_marker
694 );
695 }
696 }
697 }
698 ProjectCommands::Activate(args) => {
699 pacs.set_active_project(&args.name)
700 .with_context(|| format!("Failed to activate project '{}'", args.name))?;
701 println!("Project '{}' is now active.", args.name);
702 }
703 ProjectCommands::Deactivate => {
704 pacs.clear_active_project()
705 .context("Failed to deactivate project")?;
706 println!("Active project cleared.");
707 }
708 ProjectCommands::Active => {
709 if let Some(active) = pacs.get_active_project()? {
710 println!("{active}");
711 } else {
712 println!("No active project.");
713 }
714 }
715 },
716 Commands::Context { command } => match command {
717 ContextCommands::Add(args) => {
718 let project = if let Some(p) = args.project.clone() {
719 p
720 } else if let Some(active) = pacs.get_active_project().ok().flatten() {
721 active
722 } else {
723 anyhow::bail!("No project specified and no active project set");
724 };
725 pacs.add_context(&project, &args.name).with_context(|| {
726 format!(
727 "Failed to add context '{}' to project '{}'",
728 args.name, project
729 )
730 })?;
731 println!("Context '{}' added to project '{}'.", args.name, project);
732 }
733 ContextCommands::Remove(args) => {
734 let project = if let Some(p) = args.project.clone() {
735 p
736 } else if let Some(active) = pacs.get_active_project()? {
737 active
738 } else {
739 anyhow::bail!("No project specified and no active project set");
740 };
741 pacs.remove_context(&project, &args.name).with_context(|| {
742 format!(
743 "Failed to remove context '{}' from project '{}'",
744 args.name, project
745 )
746 })?;
747 println!(
748 "Context '{}' removed from project '{}'.",
749 args.name, project
750 );
751 }
752 ContextCommands::Edit(args) => {
753 let editor = env::var("VISUAL")
754 .ok()
755 .or_else(|| env::var("EDITOR").ok())
756 .unwrap_or_else(|| "vi".to_string());
757
758 let project = if let Some(p) = args.project.clone() {
760 p
761 } else if let Some(active) = pacs.get_active_project()? {
762 active
763 } else {
764 anyhow::bail!("No project specified and no active project set");
765 };
766
767 let project_ref = pacs
769 .projects
770 .iter()
771 .find(|p| p.name.eq_ignore_ascii_case(&project))
772 .with_context(|| format!("Project '{project}' not found"))?;
773
774 #[derive(serde::Deserialize)]
775 #[allow(clippy::items_after_statements)]
776 struct EditDoc {
777 #[serde(default)]
778 active_context: Option<String>,
779 #[serde(default)]
780 contexts: std::collections::BTreeMap<String, CtxValues>,
781 }
782 #[derive(serde::Deserialize)]
783 struct CtxValues {
784 #[serde(default)]
785 values: BTreeMap<String, String>,
786 }
787
788 let mut buf = String::new();
789 if let Some(active_ctx) = &project_ref.active_context {
790 write!(buf, "active_context = \"{active_ctx}\"\n\n").unwrap();
791 }
792
793 for ctx in &project_ref.contexts {
794 writeln!(buf, "[contexts.{}.values]", ctx.name).unwrap();
795 for (k, v) in &ctx.values {
796 writeln!(buf, "{k} = \"{}\"", v.replace('"', "\\\"")).unwrap();
797 }
798 buf.push('\n');
799 }
800
801 let temp_file =
802 std::env::temp_dir().join(format!("pacs-ctx-{}.toml", std::process::id()));
803 fs::write(&temp_file, buf)?;
804
805 let status = Command::new(&editor)
806 .arg(&temp_file)
807 .status()
808 .with_context(|| format!("Failed to open editor '{editor}'"))?;
809
810 if !status.success() {
811 fs::remove_file(&temp_file).ok();
812 anyhow::bail!("Editor exited with non-zero status");
813 }
814
815 let edited = fs::read_to_string(&temp_file)?;
816 fs::remove_file(&temp_file).ok();
817
818 let doc: EditDoc =
819 toml::from_str(&edited).with_context(|| "Failed to parse edited TOML")?;
820
821 if let Some(active_name) = doc.active_context {
822 pacs.activate_context(&project, &active_name)
823 .with_context(|| format!("Failed to set active context '{active_name}'"))?;
824 }
825
826 for (ctx_name, ctx_values) in doc.contexts {
828 pacs.edit_context_values(&project, &ctx_name, ctx_values.values.clone())
829 .with_context(|| {
830 format!(
831 "Failed to update context '{ctx_name}' values for project '{project}'"
832 )
833 })?;
834 }
835 println!("All contexts updated for project '{project}'.");
836 }
837 ContextCommands::List(args) => {
838 let project_name = if let Some(p) = args.project.clone() {
840 p
841 } else if let Some(active) = pacs.get_active_project()? {
842 active
843 } else {
844 anyhow::bail!("No project specified and no active project set");
845 };
846 let project = pacs
847 .projects
848 .iter()
849 .find(|p| p.name.eq_ignore_ascii_case(&project_name))
850 .with_context(|| format!("Project '{project_name}' not found"))?;
851 let active = project.active_context.as_ref();
852 if project.contexts.is_empty() {
853 println!("No contexts.");
854 } else {
855 for ctx in &project.contexts {
856 let active_marker = if Some(&ctx.name) == active {
857 format!(" {GREEN}*{RESET}")
858 } else {
859 String::new()
860 };
861 println!("{}{}{}{}", BLUE, ctx.name, RESET, active_marker);
862 if !ctx.values.is_empty() {
863 for (k, v) in &ctx.values {
864 println!(" {GREY}{k}:{RESET} {v}");
865 }
866 }
867 }
868 }
869 }
870 ContextCommands::Activate(args) => {
871 let project = if let Some(p) = args.project.clone() {
872 p
873 } else if let Some(active) = pacs.get_active_project()? {
874 active
875 } else {
876 anyhow::bail!("No project specified and no active project set");
877 };
878 pacs.activate_context(&project, &args.name)
879 .with_context(|| {
880 format!(
881 "Failed to activate context '{}' for project '{}'",
882 args.name, project
883 )
884 })?;
885 println!(
886 "Context '{}' activated for project '{}'.",
887 args.name, project
888 );
889 }
890 ContextCommands::Active(args) => {
891 let project = if let Some(p) = args.project.clone() {
892 p
893 } else if let Some(active) = pacs.get_active_project()? {
894 active
895 } else {
896 anyhow::bail!("No project specified and no active project set");
897 };
898 match pacs.get_active_context(&project)? {
899 Some(name) => println!("{name}"),
900 None => println!("No active context."),
901 }
902 }
903 },
904 }
905
906 Ok(())
907}
908
909#[cfg(test)]
910mod tests {
911 use super::*;
912 use clap::CommandFactory;
913
914 #[test]
915 fn verify_cli() {
916 Cli::command().debug_assert();
917 }
918}