1use clap::{Parser, ValueEnum};
9
10#[derive(Parser, Debug, Clone)]
11#[command(
12 name = "ai-usagebar",
13 version,
14 args_conflicts_with_subcommands = true,
15 about = "Waybar widget and terminal dashboard for multi-provider AI plan usage",
16 long_about = "\
17Drop-in replacement for `claudebar` with multi-vendor support.
18
19Output modes:
20 - Default: Waybar JSON ({text, tooltip, class}). Used when stdout is piped.
21 - --pretty: human-readable terminal output for local testing. Auto-enabled
22 when stdout is a TTY, so just running `ai-usagebar --vendor anthropic`
23 in a terminal Does The Right Thing.
24 - --watch N: like --pretty but refreshes every N seconds, clearing the screen
25 between ticks. Useful while iterating on `--format` or `--tooltip-format`.
26 - --json: force JSON output even when stdout is a TTY (for scripting).
27 - --config PATH: read and write an alternate config file instead of the
28 default `%APPDATA%/ai-usagebar/config.toml` (Windows) or
29 `~/.config/ai-usagebar/config.toml`. Accepted in any position, before or
30 after the subcommand; the file must already exist, and Settings saves
31 write back to it."
32)]
33pub struct Cli {
34 #[arg(long, value_enum)]
38 pub vendor: Option<Vendor>,
39
40 #[arg(long)]
43 pub icon: Option<String>,
44
45 #[arg(long)]
49 pub format: Option<String>,
50
51 #[arg(long)]
54 pub tooltip_format: Option<String>,
55
56 #[arg(long, default_value_t = 5)]
58 pub pace_tolerance: u32,
59
60 #[arg(long)]
63 pub format_pace_color: bool,
64
65 #[arg(long)]
69 pub tooltip_pace_pts: bool,
70
71 #[arg(long)]
73 pub color_low: Option<String>,
74 #[arg(long)]
76 pub color_mid: Option<String>,
77 #[arg(long)]
79 pub color_high: Option<String>,
80 #[arg(long)]
82 pub color_critical: Option<String>,
83
84 #[arg(long)]
87 pub pretty: bool,
88
89 #[arg(long, conflicts_with = "pretty")]
92 pub json: bool,
93
94 #[arg(long, value_name = "SECS")]
97 pub watch: Option<u64>,
98
99 #[arg(long, conflicts_with_all = ["cycle_prev", "watch", "pretty", "json"])]
104 pub cycle_next: bool,
105
106 #[arg(long, conflicts_with_all = ["cycle_next", "watch", "pretty", "json"])]
108 pub cycle_prev: bool,
109
110 #[arg(long, value_name = "DIR")]
114 pub cache_dir: Option<std::path::PathBuf>,
115
116 #[arg(long, value_name = "FILE")]
122 pub creds_path: Option<std::path::PathBuf>,
123
124 #[arg(long, value_name = "LABEL", conflicts_with = "creds_path")]
129 pub account: Option<String>,
130
131 #[arg(long, requires = "account")]
136 pub desktop: bool,
137
138 #[command(subcommand)]
140 pub command: Option<Command>,
141}
142
143#[derive(clap::Subcommand, Debug, Clone)]
144pub enum Command {
145 Account {
147 #[command(subcommand)]
148 action: AccountAction,
149 },
150
151 Usage {
158 #[arg(long)]
160 json: bool,
161 },
162
163 Detect {
166 #[arg(long)]
168 all: bool,
169 #[arg(long)]
171 json: bool,
172 },
173
174 Vendors {
179 #[arg(long)]
181 json: bool,
182 },
183
184 Settings {
186 #[command(subcommand)]
187 action: SettingsAction,
188 },
189
190 Auth {
192 #[command(subcommand)]
193 provider: AuthProvider,
194 },
195}
196
197#[derive(clap::Subcommand, Debug, Clone)]
198pub enum AuthProvider {
199 Nous {
200 #[command(subcommand)]
201 action: NousAuthAction,
202 },
203}
204
205#[derive(clap::Subcommand, Debug, Clone)]
206pub enum NousAuthAction {
207 Login,
209 Logout,
211}
212
213#[derive(clap::Subcommand, Debug, Clone)]
214pub enum SettingsAction {
215 Enable {
217 #[arg(value_enum)]
218 vendor: Vendor,
219 },
220
221 Show,
223
224 Apply,
226}
227
228#[derive(clap::Subcommand, Debug, Clone)]
229pub enum AccountAction {
230 Add {
232 label: String,
234
235 #[arg(long, conflicts_with = "desktop")]
237 no_login: bool,
238
239 #[arg(long)]
244 desktop: bool,
245
246 #[arg(long, requires = "desktop")]
249 email: Option<String>,
250
251 #[arg(short = 'y', long, requires = "desktop")]
253 yes: bool,
254 },
255
256 Status {
258 #[arg(long)]
260 json: bool,
261 },
262
263 Switch {
265 label: String,
268
269 #[arg(long)]
271 desktop: bool,
272
273 #[arg(long)]
275 cli: bool,
276
277 #[arg(long)]
279 dry_run: bool,
280
281 #[arg(short = 'y', long)]
283 yes: bool,
284
285 #[arg(long)]
288 force: bool,
289
290 #[arg(long)]
293 keep_bridge: bool,
294
295 #[arg(long)]
298 backup_sessions: bool,
299
300 #[arg(long, default_value_t = 10)]
302 keep_backups: usize,
303
304 #[arg(long, value_name = "KEY")]
311 delete_conflict: Vec<String>,
312 },
313}
314
315#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
316pub enum Vendor {
317 Anthropic,
318 #[value(name = "anthropic_api")]
319 AnthropicApi,
320 Openai,
321 Copilot,
322 Zai,
323 Openrouter,
324 Deepseek,
325 Kimi,
326 Kilo,
327 Novita,
328 Moonshot,
329 Grok,
330 Supergrok,
331 Grokbot,
332 Antigravity,
333 Cursor,
334 Minimax,
335 Kiro,
336 #[value(name = "nous")]
337 NousResearch,
338 #[value(name = "opencode-go")]
339 OpenCodeGo,
340 #[value(name = "commandcode")]
341 CommandCode,
342 Ollama,
343 OrcaRouter,
344 #[value(name = "modelstudio")]
345 ModelStudio,
346}
347
348impl Vendor {
349 pub fn to_id(self) -> crate::vendor::VendorId {
350 match self {
351 Vendor::Anthropic => crate::vendor::VendorId::Anthropic,
352 Vendor::AnthropicApi => crate::vendor::VendorId::AnthropicApi,
353 Vendor::Openai => crate::vendor::VendorId::Openai,
354 Vendor::Copilot => crate::vendor::VendorId::Copilot,
355 Vendor::Zai => crate::vendor::VendorId::Zai,
356 Vendor::Openrouter => crate::vendor::VendorId::Openrouter,
357 Vendor::Deepseek => crate::vendor::VendorId::Deepseek,
358 Vendor::Kimi => crate::vendor::VendorId::Kimi,
359 Vendor::Kilo => crate::vendor::VendorId::Kilo,
360 Vendor::Novita => crate::vendor::VendorId::Novita,
361 Vendor::Moonshot => crate::vendor::VendorId::Moonshot,
362 Vendor::Grok => crate::vendor::VendorId::Grok,
363 Vendor::Supergrok => crate::vendor::VendorId::Supergrok,
364 Vendor::Grokbot => crate::vendor::VendorId::Grokbot,
365 Vendor::Antigravity => crate::vendor::VendorId::Antigravity,
366 Vendor::Cursor => crate::vendor::VendorId::Cursor,
367 Vendor::Minimax => crate::vendor::VendorId::Minimax,
368 Vendor::Kiro => crate::vendor::VendorId::Kiro,
369 Vendor::NousResearch => crate::vendor::VendorId::NousResearch,
370 Vendor::OpenCodeGo => crate::vendor::VendorId::OpenCodeGo,
371 Vendor::CommandCode => crate::vendor::VendorId::CommandCode,
372 Vendor::Ollama => crate::vendor::VendorId::Ollama,
373 Vendor::OrcaRouter => crate::vendor::VendorId::OrcaRouter,
374 Vendor::ModelStudio => crate::vendor::VendorId::ModelStudio,
375 }
376 }
377}
378
379impl Cli {
380 pub fn has_explicit_vendor(&self) -> bool {
382 self.vendor.is_some()
383 }
384
385 pub fn resolved_vendor(&self, config: &crate::config::Config) -> Vendor {
396 let active = if self.has_explicit_vendor() {
402 None
403 } else {
404 crate::active::read()
405 };
406 self.resolve_vendor_with(config, active)
407 }
408
409 pub fn resolve_vendor_with(
414 &self,
415 config: &crate::config::Config,
416 active: Option<crate::vendor::VendorId>,
417 ) -> Vendor {
418 if let Some(v) = self.vendor {
419 return v;
420 }
421 if let Some(id) = active
422 && config.is_enabled(id)
423 {
424 return id_to_vendor(id);
425 }
426 if let Some(id) = config.ui.primary
427 && config.is_enabled(id)
428 {
429 return id_to_vendor(id);
430 }
431 if config.is_enabled(crate::vendor::VendorId::Anthropic) {
432 return Vendor::Anthropic;
433 }
434 config
435 .enabled_vendors()
436 .into_iter()
437 .next()
438 .map(id_to_vendor)
439 .unwrap_or(Vendor::Anthropic)
442 }
443}
444
445fn id_to_vendor(id: crate::vendor::VendorId) -> Vendor {
446 match id {
447 crate::vendor::VendorId::Anthropic => Vendor::Anthropic,
448 crate::vendor::VendorId::AnthropicApi => Vendor::AnthropicApi,
449 crate::vendor::VendorId::Openai => Vendor::Openai,
450 crate::vendor::VendorId::Copilot => Vendor::Copilot,
451 crate::vendor::VendorId::Zai => Vendor::Zai,
452 crate::vendor::VendorId::Openrouter => Vendor::Openrouter,
453 crate::vendor::VendorId::Deepseek => Vendor::Deepseek,
454 crate::vendor::VendorId::Kimi => Vendor::Kimi,
455 crate::vendor::VendorId::Kilo => Vendor::Kilo,
456 crate::vendor::VendorId::Novita => Vendor::Novita,
457 crate::vendor::VendorId::Moonshot => Vendor::Moonshot,
458 crate::vendor::VendorId::Grok => Vendor::Grok,
459 crate::vendor::VendorId::Supergrok => Vendor::Supergrok,
460 crate::vendor::VendorId::Grokbot => Vendor::Grokbot,
461 crate::vendor::VendorId::Antigravity => Vendor::Antigravity,
462 crate::vendor::VendorId::Cursor => Vendor::Cursor,
463 crate::vendor::VendorId::Minimax => Vendor::Minimax,
464 crate::vendor::VendorId::Kiro => Vendor::Kiro,
465 crate::vendor::VendorId::NousResearch => Vendor::NousResearch,
466 crate::vendor::VendorId::OpenCodeGo => Vendor::OpenCodeGo,
467 crate::vendor::VendorId::CommandCode => Vendor::CommandCode,
468 crate::vendor::VendorId::Ollama => Vendor::Ollama,
469 crate::vendor::VendorId::OrcaRouter => Vendor::OrcaRouter,
470 crate::vendor::VendorId::ModelStudio => Vendor::ModelStudio,
471 }
472}
473
474impl Cli {
475 pub fn output_json(&self) -> bool {
478 if self.json {
479 return true;
480 }
481 if self.pretty || self.watch.is_some() {
482 return false;
483 }
484 !is_stdout_tty()
486 }
487}
488
489fn is_stdout_tty() -> bool {
490 use std::io::IsTerminal;
491 std::io::stdout().is_terminal()
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497 use clap::{Parser, error::ErrorKind};
498
499 #[test]
500 fn settings_enable_requires_a_known_vendor() {
501 assert!(matches!(
502 Cli::try_parse_from(["ai-usagebar", "settings", "enable", "anthropic"])
503 .unwrap()
504 .command,
505 Some(Command::Settings {
506 action: SettingsAction::Enable {
507 vendor: Vendor::Anthropic
508 }
509 })
510 ));
511 assert!(Cli::try_parse_from(["ai-usagebar", "settings", "enable", "unknown"]).is_err());
512 assert!(Cli::try_parse_from(["ai-usagebar", "settings", "enable"]).is_err());
513 }
514
515 #[test]
516 fn version_flags_report_the_crate_version() {
517 let expected = format!("ai-usagebar {}\n", env!("CARGO_PKG_VERSION"));
518
519 for flag in ["--version", "-V"] {
520 let err = Cli::try_parse_from(["ai-usagebar", flag])
521 .expect_err("a version flag exits through clap's display path");
522 assert_eq!(err.kind(), ErrorKind::DisplayVersion, "flag: {flag}");
523 assert_eq!(err.to_string(), expected, "flag: {flag}");
524 }
525 }
526
527 #[test]
528 fn usage_subcommand_parses_machine_readable_mode() {
529 let cli = Cli::parse_from(["ai-usagebar", "usage", "--json"]);
530 assert!(matches!(cli.command, Some(Command::Usage { json: true })));
531 }
532
533 #[test]
534 fn usage_help_states_complete_document_exits_zero() {
535 let err = Cli::try_parse_from(["ai-usagebar", "usage", "--help"])
536 .expect_err("help exits through clap's display path");
537 assert_eq!(err.kind(), ErrorKind::DisplayHelp);
538 let help = err.to_string();
539 assert!(
540 help.contains("Exits 0 after printing a complete document"),
541 "{help}"
542 );
543 assert!(
544 help.contains("even when every entry") && help.contains("error"),
545 "{help}"
546 );
547 assert!(
548 help.contains("Non-zero only when the document cannot be produced"),
549 "{help}"
550 );
551 }
552
553 #[test]
554 fn detect_subcommand_parses_its_flags_and_takes_no_widget_flags() {
555 let bare = Cli::parse_from(["ai-usagebar", "detect"]);
556 assert!(matches!(
557 bare.command,
558 Some(Command::Detect {
559 all: false,
560 json: false
561 })
562 ));
563
564 let full = Cli::parse_from(["ai-usagebar", "detect", "--all", "--json"]);
565 assert!(matches!(
566 full.command,
567 Some(Command::Detect {
568 all: true,
569 json: true
570 })
571 ));
572
573 assert!(Cli::try_parse_from(["ai-usagebar", "--vendor", "kimi", "detect"]).is_err());
574 }
575
576 #[test]
577 fn new_vendor_values_and_auth_commands_parse_exactly() {
578 let nous = Cli::parse_from(["ai-usagebar", "--vendor", "nous"]);
579 assert_eq!(nous.vendor, Some(Vendor::NousResearch));
580 let opencode = Cli::parse_from(["ai-usagebar", "--vendor", "opencode-go"]);
581 assert_eq!(opencode.vendor, Some(Vendor::OpenCodeGo));
582 let copilot = Cli::parse_from(["ai-usagebar", "--vendor", "copilot"]);
583 assert_eq!(copilot.vendor, Some(Vendor::Copilot));
584 let login = Cli::parse_from(["ai-usagebar", "auth", "nous", "login"]);
585 assert!(matches!(login.command, Some(Command::Auth { .. })));
586 }
587
588 #[test]
589 fn settings_subcommands_are_additive_and_take_no_widget_flags() {
590 let show = Cli::parse_from(["ai-usagebar", "settings", "show"]);
591 assert!(matches!(
592 show.command,
593 Some(Command::Settings {
594 action: SettingsAction::Show
595 })
596 ));
597
598 let apply = Cli::parse_from(["ai-usagebar", "settings", "apply"]);
599 assert!(matches!(
600 apply.command,
601 Some(Command::Settings {
602 action: SettingsAction::Apply
603 })
604 ));
605
606 assert!(
607 Cli::try_parse_from(["ai-usagebar", "--vendor", "kimi", "settings", "show",]).is_err()
608 );
609 }
610
611 #[test]
612 fn defaults_match_claudebar() {
613 let cli = Cli::parse_from(["ai-usagebar"]);
614 assert_eq!(cli.vendor, None);
615 let cfg = crate::config::Config::default();
620 assert_eq!(cli.resolve_vendor_with(&cfg, None), Vendor::Anthropic);
621 assert_eq!(cli.pace_tolerance, 5);
622 assert!(cli.format.is_none());
623 assert!(cli.tooltip_format.is_none());
624 assert!(cli.icon.is_none());
625 assert!(!cli.format_pace_color);
626 assert!(!cli.tooltip_pace_pts);
627 assert!(!cli.pretty);
628 assert!(!cli.json);
629 assert!(cli.watch.is_none());
630 assert!(cli.command.is_none());
631 }
632
633 #[test]
634 fn account_add_subcommand_parses_without_widget_flags() {
635 let cli = Cli::parse_from(["ai-usagebar", "account", "add", "work", "--no-login"]);
636 assert!(matches!(
637 cli.command,
638 Some(Command::Account {
639 action: AccountAction::Add {
640 ref label,
641 no_login: true,
642 desktop: false,
643 ..
644 }
645 }) if label == "work"
646 ));
647 }
648
649 #[test]
652 fn account_add_desktop_takes_an_email_and_rejects_no_login() {
653 let cli = Cli::parse_from([
654 "ai-usagebar",
655 "account",
656 "add",
657 "work",
658 "--desktop",
659 "--email",
660 "a@b.test",
661 "-y",
662 ]);
663 assert!(matches!(
664 cli.command,
665 Some(Command::Account {
666 action: AccountAction::Add {
667 desktop: true,
668 yes: true,
669 email: Some(ref email),
670 ..
671 }
672 }) if email == "a@b.test"
673 ));
674 assert!(
675 Cli::try_parse_from([
676 "ai-usagebar",
677 "account",
678 "add",
679 "w",
680 "--desktop",
681 "--no-login"
682 ])
683 .is_err()
684 );
685 assert!(
687 Cli::try_parse_from(["ai-usagebar", "account", "add", "w", "--email", "a@b.test"])
688 .is_err()
689 );
690 }
691
692 #[test]
693 fn account_switch_defaults_to_both_surfaces() {
694 let cli = Cli::parse_from(["ai-usagebar", "account", "switch", "work", "--dry-run"]);
695 assert!(matches!(
696 cli.command,
697 Some(Command::Account {
698 action: AccountAction::Switch {
699 ref label,
700 desktop: false,
701 cli: false,
702 dry_run: true,
703 keep_backups: 10,
704 ..
705 }
706 }) if label == "work"
707 ));
708 }
709
710 #[test]
711 fn account_subcommand_rejects_ignored_widget_flags() {
712 assert!(
713 Cli::try_parse_from([
714 "ai-usagebar",
715 "--vendor",
716 "anthropic",
717 "account",
718 "add",
719 "work",
720 ])
721 .is_err()
722 );
723 }
724
725 #[test]
726 fn multi_account_flags_are_stable_api() {
727 let cli = Cli::parse_from([
731 "ai-usagebar",
732 "--vendor",
733 "anthropic",
734 "--cache-dir",
735 "/tmp/acct-a",
736 "--creds-path",
737 "/tmp/acct-a/credentials.json",
738 ]);
739 assert_eq!(
740 cli.cache_dir.as_deref(),
741 Some(std::path::Path::new("/tmp/acct-a"))
742 );
743 assert_eq!(
744 cli.creds_path.as_deref(),
745 Some(std::path::Path::new("/tmp/acct-a/credentials.json"))
746 );
747 }
748
749 #[test]
750 fn primary_from_config_wins_when_vendor_unset() {
751 let cli = Cli::parse_from(["ai-usagebar"]);
753 let mut cfg = crate::config::Config::default();
754 cfg.ui.primary = Some(crate::vendor::VendorId::Openrouter);
755 assert_eq!(cli.resolve_vendor_with(&cfg, None), Vendor::Openrouter);
756 }
757
758 #[test]
759 fn explicit_vendor_overrides_everything() {
760 let cli = Cli::parse_from(["ai-usagebar", "--vendor", "zai"]);
763 let mut cfg = crate::config::Config::default();
764 cfg.ui.primary = Some(crate::vendor::VendorId::Openrouter);
765 let active = Some(crate::vendor::VendorId::Openai);
766 assert_eq!(cli.resolve_vendor_with(&cfg, active), Vendor::Zai);
767 }
768
769 #[test]
770 fn vendor_kimi_parses_to_kimi_variant() {
771 let cli = Cli::parse_from(["ai-usagebar", "--vendor", "kimi"]);
772 assert_eq!(cli.vendor, Some(Vendor::Kimi));
773 assert_eq!(cli.vendor.unwrap().to_id(), crate::vendor::VendorId::Kimi);
774 }
775
776 #[test]
777 fn vendor_grokbot_parses_to_grokbot_variant() {
778 let cli = Cli::parse_from(["ai-usagebar", "--vendor", "grokbot"]);
779 assert_eq!(cli.vendor, Some(Vendor::Grokbot));
780 assert_eq!(
781 cli.vendor.unwrap().to_id(),
782 crate::vendor::VendorId::Grokbot
783 );
784 assert_eq!(
786 id_to_vendor(crate::vendor::VendorId::Grokbot),
787 Vendor::Grokbot
788 );
789 }
790
791 #[test]
792 fn vendor_modelstudio_parses_to_modelstudio_variant() {
793 let cli = Cli::parse_from(["ai-usagebar", "--vendor", "modelstudio"]);
794 assert_eq!(cli.vendor, Some(Vendor::ModelStudio));
795 assert_eq!(
796 cli.vendor.unwrap().to_id(),
797 crate::vendor::VendorId::ModelStudio
798 );
799 assert_eq!(
800 id_to_vendor(crate::vendor::VendorId::ModelStudio),
801 Vendor::ModelStudio
802 );
803 }
804
805 #[test]
806 fn vendor_anthropic_api_uses_the_documented_slug() {
807 let cli = Cli::parse_from(["ai-usagebar", "--vendor", "anthropic_api"]);
808 assert_eq!(cli.vendor, Some(Vendor::AnthropicApi));
809 assert_eq!(
810 cli.vendor.unwrap().to_id(),
811 crate::vendor::VendorId::AnthropicApi
812 );
813 }
814
815 #[test]
816 fn disabled_kimi_primary_falls_back_to_an_enabled_vendor() {
817 let cli = Cli::parse_from(["ai-usagebar"]);
818 let mut cfg = crate::config::Config::default();
819 cfg.ui.primary = Some(crate::vendor::VendorId::Kimi);
820 assert_eq!(cli.resolve_vendor_with(&cfg, None), Vendor::Anthropic);
821 }
822
823 #[test]
824 fn explicit_kimi_remains_an_opt_in_override_when_disabled() {
825 let cli = Cli::parse_from(["ai-usagebar", "--vendor", "kimi"]);
826 assert_eq!(
827 cli.resolve_vendor_with(&crate::config::Config::default(), None),
828 Vendor::Kimi
829 );
830 }
831
832 #[test]
833 fn active_override_wins_over_config_primary_when_enabled() {
834 let cli = Cli::parse_from(["ai-usagebar"]);
837 let mut cfg = crate::config::Config::default();
838 cfg.ui.primary = Some(crate::vendor::VendorId::Openrouter);
839 let active = Some(crate::vendor::VendorId::Zai);
840 assert_eq!(cli.resolve_vendor_with(&cfg, active), Vendor::Zai);
841 }
842
843 #[test]
844 fn disabled_active_override_falls_back_to_config_primary() {
845 let cli = Cli::parse_from(["ai-usagebar"]);
848 let mut cfg = crate::config::Config::default();
849 cfg.zai.enabled = false;
850 cfg.ui.primary = Some(crate::vendor::VendorId::Openrouter);
851 let active = Some(crate::vendor::VendorId::Zai);
852 assert_eq!(cli.resolve_vendor_with(&cfg, active), Vendor::Openrouter);
853 }
854
855 #[test]
856 fn claudebar_compatible_flag_surface() {
857 let cli = Cli::parse_from([
858 "ai-usagebar",
859 "--icon",
860 "",
861 "--format",
862 "{session_pct}% · {session_reset}",
863 "--tooltip-format",
864 "S:{session_pct}",
865 "--pace-tolerance",
866 "10",
867 "--format-pace-color",
868 "--tooltip-pace-pts",
869 "--color-low",
870 "#50fa7b",
871 "--color-mid",
872 "#f1fa8c",
873 "--color-high",
874 "#ffb86c",
875 "--color-critical",
876 "#ff5555",
877 ]);
878 assert_eq!(cli.icon.as_deref(), Some(""));
879 assert_eq!(
880 cli.format.as_deref(),
881 Some("{session_pct}% · {session_reset}")
882 );
883 assert_eq!(cli.tooltip_format.as_deref(), Some("S:{session_pct}"));
884 assert_eq!(cli.pace_tolerance, 10);
885 assert!(cli.format_pace_color);
886 assert!(cli.tooltip_pace_pts);
887 assert_eq!(cli.color_low.as_deref(), Some("#50fa7b"));
888 assert_eq!(cli.color_critical.as_deref(), Some("#ff5555"));
889 }
890
891 #[test]
892 fn pretty_and_json_conflict() {
893 let res = Cli::try_parse_from(["ai-usagebar", "--pretty", "--json"]);
894 assert!(res.is_err());
895 }
896
897 #[test]
898 fn watch_disables_json_output() {
899 let cli = Cli::parse_from(["ai-usagebar", "--watch", "5"]);
900 assert_eq!(cli.watch, Some(5));
901 assert!(!cli.output_json());
902 }
903}