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")]
131 pub account: Option<String>,
132
133 #[arg(long, requires = "account")]
138 pub desktop: bool,
139
140 #[command(subcommand)]
142 pub command: Option<Command>,
143}
144
145#[derive(clap::Subcommand, Debug, Clone)]
146pub enum Command {
147 Account {
149 #[command(subcommand)]
150 action: AccountAction,
151 },
152
153 Usage {
160 #[arg(long)]
162 json: bool,
163 },
164
165 Detect {
168 #[arg(long)]
170 all: bool,
171 #[arg(long)]
173 json: bool,
174 },
175
176 Vendors {
181 #[arg(long)]
183 json: bool,
184 },
185
186 Settings {
188 #[command(subcommand)]
189 action: SettingsAction,
190 },
191
192 Auth {
194 #[command(subcommand)]
195 provider: AuthProvider,
196 },
197}
198
199#[derive(clap::Subcommand, Debug, Clone)]
200pub enum AuthProvider {
201 Nous {
202 #[command(subcommand)]
203 action: NousAuthAction,
204 },
205}
206
207#[derive(clap::Subcommand, Debug, Clone)]
208pub enum NousAuthAction {
209 Login,
211 Logout,
213}
214
215#[derive(clap::Subcommand, Debug, Clone)]
216pub enum SettingsAction {
217 Enable {
219 #[arg(value_enum)]
220 vendor: Vendor,
221 },
222
223 Show,
225
226 Apply,
228}
229
230#[derive(clap::Subcommand, Debug, Clone)]
231pub enum AccountAction {
232 Add {
234 label: String,
236
237 #[arg(long, conflicts_with = "desktop")]
239 no_login: bool,
240
241 #[arg(long)]
246 desktop: bool,
247
248 #[arg(long, requires = "desktop")]
251 email: Option<String>,
252
253 #[arg(short = 'y', long, requires = "desktop")]
255 yes: bool,
256
257 #[arg(long, conflicts_with = "desktop")]
261 codex: bool,
262
263 #[arg(long, conflicts_with_all = ["desktop", "no_login"])]
267 adopt_current: bool,
268 },
269
270 Status {
273 #[arg(long)]
275 json: bool,
276 },
277
278 Switch {
281 label: String,
285
286 #[arg(
289 long,
290 conflicts_with_all = ["desktop", "cli", "keep_bridge", "backup_sessions", "delete_conflict"]
291 )]
292 codex: bool,
293
294 #[arg(long)]
296 desktop: bool,
297
298 #[arg(long)]
300 cli: bool,
301
302 #[arg(long)]
304 dry_run: bool,
305
306 #[arg(short = 'y', long)]
308 yes: bool,
309
310 #[arg(long)]
314 force: bool,
315
316 #[arg(long)]
319 keep_bridge: bool,
320
321 #[arg(long)]
324 backup_sessions: bool,
325
326 #[arg(long, default_value_t = 10)]
328 keep_backups: usize,
329
330 #[arg(long, value_name = "KEY")]
337 delete_conflict: Vec<String>,
338 },
339
340 MergeHistory {
355 #[arg(long, value_name = "DIR")]
359 data_dir: std::path::PathBuf,
360
361 #[arg(long, value_name = "DIR")]
368 from: Vec<std::path::PathBuf>,
369
370 #[arg(long)]
372 dry_run: bool,
373 },
374}
375
376#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
377pub enum Vendor {
378 Anthropic,
379 #[value(name = "anthropic_api")]
380 AnthropicApi,
381 Openai,
382 Copilot,
383 Zai,
384 Openrouter,
385 Deepseek,
386 Kimi,
387 Kilo,
388 Novita,
389 Moonshot,
390 Grok,
391 Supergrok,
392 Grokbot,
393 Antigravity,
394 Cursor,
395 Minimax,
396 Kiro,
397 #[value(name = "nous")]
398 NousResearch,
399 #[value(name = "opencode-go")]
400 OpenCodeGo,
401 #[value(name = "commandcode")]
402 CommandCode,
403 Ollama,
404 #[value(name = "orcarouter", alias = "orca-router")]
407 OrcaRouter,
408 #[value(name = "modelstudio")]
409 ModelStudio,
410}
411
412impl Vendor {
413 pub fn to_id(self) -> crate::vendor::VendorId {
414 match self {
415 Vendor::Anthropic => crate::vendor::VendorId::Anthropic,
416 Vendor::AnthropicApi => crate::vendor::VendorId::AnthropicApi,
417 Vendor::Openai => crate::vendor::VendorId::Openai,
418 Vendor::Copilot => crate::vendor::VendorId::Copilot,
419 Vendor::Zai => crate::vendor::VendorId::Zai,
420 Vendor::Openrouter => crate::vendor::VendorId::Openrouter,
421 Vendor::Deepseek => crate::vendor::VendorId::Deepseek,
422 Vendor::Kimi => crate::vendor::VendorId::Kimi,
423 Vendor::Kilo => crate::vendor::VendorId::Kilo,
424 Vendor::Novita => crate::vendor::VendorId::Novita,
425 Vendor::Moonshot => crate::vendor::VendorId::Moonshot,
426 Vendor::Grok => crate::vendor::VendorId::Grok,
427 Vendor::Supergrok => crate::vendor::VendorId::Supergrok,
428 Vendor::Grokbot => crate::vendor::VendorId::Grokbot,
429 Vendor::Antigravity => crate::vendor::VendorId::Antigravity,
430 Vendor::Cursor => crate::vendor::VendorId::Cursor,
431 Vendor::Minimax => crate::vendor::VendorId::Minimax,
432 Vendor::Kiro => crate::vendor::VendorId::Kiro,
433 Vendor::NousResearch => crate::vendor::VendorId::NousResearch,
434 Vendor::OpenCodeGo => crate::vendor::VendorId::OpenCodeGo,
435 Vendor::CommandCode => crate::vendor::VendorId::CommandCode,
436 Vendor::Ollama => crate::vendor::VendorId::Ollama,
437 Vendor::OrcaRouter => crate::vendor::VendorId::OrcaRouter,
438 Vendor::ModelStudio => crate::vendor::VendorId::ModelStudio,
439 }
440 }
441}
442
443impl Cli {
444 pub fn has_explicit_vendor(&self) -> bool {
446 self.vendor.is_some()
447 }
448
449 pub fn resolved_vendor(&self, config: &crate::config::Config) -> Vendor {
460 let active = if self.has_explicit_vendor() {
466 None
467 } else {
468 crate::active::read()
469 };
470 self.resolve_vendor_with(config, active)
471 }
472
473 pub fn resolve_vendor_with(
478 &self,
479 config: &crate::config::Config,
480 active: Option<crate::vendor::VendorId>,
481 ) -> Vendor {
482 if let Some(v) = self.vendor {
483 return v;
484 }
485 if let Some(id) = active
486 && config.is_enabled(id)
487 {
488 return id_to_vendor(id);
489 }
490 if let Some(id) = config.ui.primary
491 && config.is_enabled(id)
492 {
493 return id_to_vendor(id);
494 }
495 if config.is_enabled(crate::vendor::VendorId::Anthropic) {
496 return Vendor::Anthropic;
497 }
498 config
499 .enabled_vendors()
500 .into_iter()
501 .next()
502 .map(id_to_vendor)
503 .unwrap_or(Vendor::Anthropic)
506 }
507}
508
509fn id_to_vendor(id: crate::vendor::VendorId) -> Vendor {
510 match id {
511 crate::vendor::VendorId::Anthropic => Vendor::Anthropic,
512 crate::vendor::VendorId::AnthropicApi => Vendor::AnthropicApi,
513 crate::vendor::VendorId::Openai => Vendor::Openai,
514 crate::vendor::VendorId::Copilot => Vendor::Copilot,
515 crate::vendor::VendorId::Zai => Vendor::Zai,
516 crate::vendor::VendorId::Openrouter => Vendor::Openrouter,
517 crate::vendor::VendorId::Deepseek => Vendor::Deepseek,
518 crate::vendor::VendorId::Kimi => Vendor::Kimi,
519 crate::vendor::VendorId::Kilo => Vendor::Kilo,
520 crate::vendor::VendorId::Novita => Vendor::Novita,
521 crate::vendor::VendorId::Moonshot => Vendor::Moonshot,
522 crate::vendor::VendorId::Grok => Vendor::Grok,
523 crate::vendor::VendorId::Supergrok => Vendor::Supergrok,
524 crate::vendor::VendorId::Grokbot => Vendor::Grokbot,
525 crate::vendor::VendorId::Antigravity => Vendor::Antigravity,
526 crate::vendor::VendorId::Cursor => Vendor::Cursor,
527 crate::vendor::VendorId::Minimax => Vendor::Minimax,
528 crate::vendor::VendorId::Kiro => Vendor::Kiro,
529 crate::vendor::VendorId::NousResearch => Vendor::NousResearch,
530 crate::vendor::VendorId::OpenCodeGo => Vendor::OpenCodeGo,
531 crate::vendor::VendorId::CommandCode => Vendor::CommandCode,
532 crate::vendor::VendorId::Ollama => Vendor::Ollama,
533 crate::vendor::VendorId::OrcaRouter => Vendor::OrcaRouter,
534 crate::vendor::VendorId::ModelStudio => Vendor::ModelStudio,
535 }
536}
537
538impl Cli {
539 pub fn output_json(&self) -> bool {
542 if self.json {
543 return true;
544 }
545 if self.pretty || self.watch.is_some() {
546 return false;
547 }
548 !is_stdout_tty()
550 }
551}
552
553fn is_stdout_tty() -> bool {
554 use std::io::IsTerminal;
555 std::io::stdout().is_terminal()
556}
557
558#[cfg(test)]
559mod tests {
560 use super::*;
561 use clap::{Parser, error::ErrorKind};
562
563 #[test]
564 fn settings_enable_requires_a_known_vendor() {
565 assert!(matches!(
566 Cli::try_parse_from(["ai-usagebar", "settings", "enable", "anthropic"])
567 .unwrap()
568 .command,
569 Some(Command::Settings {
570 action: SettingsAction::Enable {
571 vendor: Vendor::Anthropic
572 }
573 })
574 ));
575 assert!(Cli::try_parse_from(["ai-usagebar", "settings", "enable", "unknown"]).is_err());
576 assert!(Cli::try_parse_from(["ai-usagebar", "settings", "enable"]).is_err());
577 }
578
579 #[test]
580 fn version_flags_report_the_crate_version() {
581 let expected = format!("ai-usagebar {}\n", env!("CARGO_PKG_VERSION"));
582
583 for flag in ["--version", "-V"] {
584 let err = Cli::try_parse_from(["ai-usagebar", flag])
585 .expect_err("a version flag exits through clap's display path");
586 assert_eq!(err.kind(), ErrorKind::DisplayVersion, "flag: {flag}");
587 assert_eq!(err.to_string(), expected, "flag: {flag}");
588 }
589 }
590
591 #[test]
592 fn usage_subcommand_parses_machine_readable_mode() {
593 let cli = Cli::parse_from(["ai-usagebar", "usage", "--json"]);
594 assert!(matches!(cli.command, Some(Command::Usage { json: true })));
595 }
596
597 #[test]
598 fn usage_help_states_complete_document_exits_zero() {
599 let err = Cli::try_parse_from(["ai-usagebar", "usage", "--help"])
600 .expect_err("help exits through clap's display path");
601 assert_eq!(err.kind(), ErrorKind::DisplayHelp);
602 let help = err.to_string();
603 assert!(
604 help.contains("Exits 0 after printing a complete document"),
605 "{help}"
606 );
607 assert!(
608 help.contains("even when every entry") && help.contains("error"),
609 "{help}"
610 );
611 assert!(
612 help.contains("Non-zero only when the document cannot be produced"),
613 "{help}"
614 );
615 }
616
617 #[test]
618 fn detect_subcommand_parses_its_flags_and_takes_no_widget_flags() {
619 let bare = Cli::parse_from(["ai-usagebar", "detect"]);
620 assert!(matches!(
621 bare.command,
622 Some(Command::Detect {
623 all: false,
624 json: false
625 })
626 ));
627
628 let full = Cli::parse_from(["ai-usagebar", "detect", "--all", "--json"]);
629 assert!(matches!(
630 full.command,
631 Some(Command::Detect {
632 all: true,
633 json: true
634 })
635 ));
636
637 assert!(Cli::try_parse_from(["ai-usagebar", "--vendor", "kimi", "detect"]).is_err());
638 }
639
640 #[test]
641 fn new_vendor_values_and_auth_commands_parse_exactly() {
642 let nous = Cli::parse_from(["ai-usagebar", "--vendor", "nous"]);
643 assert_eq!(nous.vendor, Some(Vendor::NousResearch));
644 let opencode = Cli::parse_from(["ai-usagebar", "--vendor", "opencode-go"]);
645 assert_eq!(opencode.vendor, Some(Vendor::OpenCodeGo));
646 let copilot = Cli::parse_from(["ai-usagebar", "--vendor", "copilot"]);
647 assert_eq!(copilot.vendor, Some(Vendor::Copilot));
648 let login = Cli::parse_from(["ai-usagebar", "auth", "nous", "login"]);
649 assert!(matches!(login.command, Some(Command::Auth { .. })));
650 }
651
652 #[test]
653 fn settings_subcommands_are_additive_and_take_no_widget_flags() {
654 let show = Cli::parse_from(["ai-usagebar", "settings", "show"]);
655 assert!(matches!(
656 show.command,
657 Some(Command::Settings {
658 action: SettingsAction::Show
659 })
660 ));
661
662 let apply = Cli::parse_from(["ai-usagebar", "settings", "apply"]);
663 assert!(matches!(
664 apply.command,
665 Some(Command::Settings {
666 action: SettingsAction::Apply
667 })
668 ));
669
670 assert!(
671 Cli::try_parse_from(["ai-usagebar", "--vendor", "kimi", "settings", "show",]).is_err()
672 );
673 }
674
675 #[test]
676 fn defaults_match_claudebar() {
677 let cli = Cli::parse_from(["ai-usagebar"]);
678 assert_eq!(cli.vendor, None);
679 let cfg = crate::config::Config::default();
684 assert_eq!(cli.resolve_vendor_with(&cfg, None), Vendor::Anthropic);
685 assert_eq!(cli.pace_tolerance, 5);
686 assert!(cli.format.is_none());
687 assert!(cli.tooltip_format.is_none());
688 assert!(cli.icon.is_none());
689 assert!(!cli.format_pace_color);
690 assert!(!cli.tooltip_pace_pts);
691 assert!(!cli.pretty);
692 assert!(!cli.json);
693 assert!(cli.watch.is_none());
694 assert!(cli.command.is_none());
695 }
696
697 #[test]
698 fn account_add_subcommand_parses_without_widget_flags() {
699 let cli = Cli::parse_from(["ai-usagebar", "account", "add", "work", "--no-login"]);
700 assert!(matches!(
701 cli.command,
702 Some(Command::Account {
703 action: AccountAction::Add {
704 ref label,
705 no_login: true,
706 desktop: false,
707 ..
708 }
709 }) if label == "work"
710 ));
711 }
712
713 #[test]
716 fn account_add_desktop_takes_an_email_and_rejects_no_login() {
717 let cli = Cli::parse_from([
718 "ai-usagebar",
719 "account",
720 "add",
721 "work",
722 "--desktop",
723 "--email",
724 "a@b.test",
725 "-y",
726 ]);
727 assert!(matches!(
728 cli.command,
729 Some(Command::Account {
730 action: AccountAction::Add {
731 desktop: true,
732 yes: true,
733 email: Some(ref email),
734 ..
735 }
736 }) if email == "a@b.test"
737 ));
738 assert!(
739 Cli::try_parse_from([
740 "ai-usagebar",
741 "account",
742 "add",
743 "w",
744 "--desktop",
745 "--no-login"
746 ])
747 .is_err()
748 );
749 assert!(
751 Cli::try_parse_from(["ai-usagebar", "account", "add", "w", "--email", "a@b.test"])
752 .is_err()
753 );
754 }
755
756 #[test]
757 fn account_switch_defaults_to_both_surfaces() {
758 let cli = Cli::parse_from(["ai-usagebar", "account", "switch", "work", "--dry-run"]);
759 assert!(matches!(
760 cli.command,
761 Some(Command::Account {
762 action: AccountAction::Switch {
763 ref label,
764 desktop: false,
765 cli: false,
766 dry_run: true,
767 keep_backups: 10,
768 ..
769 }
770 }) if label == "work"
771 ));
772 }
773
774 #[test]
775 fn account_subcommand_rejects_ignored_widget_flags() {
776 assert!(
777 Cli::try_parse_from([
778 "ai-usagebar",
779 "--vendor",
780 "anthropic",
781 "account",
782 "add",
783 "work",
784 ])
785 .is_err()
786 );
787 }
788
789 #[test]
790 fn multi_account_flags_are_stable_api() {
791 let cli = Cli::parse_from([
795 "ai-usagebar",
796 "--vendor",
797 "anthropic",
798 "--cache-dir",
799 "/tmp/acct-a",
800 "--creds-path",
801 "/tmp/acct-a/credentials.json",
802 ]);
803 assert_eq!(
804 cli.cache_dir.as_deref(),
805 Some(std::path::Path::new("/tmp/acct-a"))
806 );
807 assert_eq!(
808 cli.creds_path.as_deref(),
809 Some(std::path::Path::new("/tmp/acct-a/credentials.json"))
810 );
811 }
812
813 #[test]
814 fn primary_from_config_wins_when_vendor_unset() {
815 let cli = Cli::parse_from(["ai-usagebar"]);
817 let mut cfg = crate::config::Config::default();
818 cfg.ui.primary = Some(crate::vendor::VendorId::Openrouter);
819 assert_eq!(cli.resolve_vendor_with(&cfg, None), Vendor::Openrouter);
820 }
821
822 #[test]
823 fn explicit_vendor_overrides_everything() {
824 let cli = Cli::parse_from(["ai-usagebar", "--vendor", "zai"]);
827 let mut cfg = crate::config::Config::default();
828 cfg.ui.primary = Some(crate::vendor::VendorId::Openrouter);
829 let active = Some(crate::vendor::VendorId::Openai);
830 assert_eq!(cli.resolve_vendor_with(&cfg, active), Vendor::Zai);
831 }
832
833 #[test]
834 fn vendor_kimi_parses_to_kimi_variant() {
835 let cli = Cli::parse_from(["ai-usagebar", "--vendor", "kimi"]);
836 assert_eq!(cli.vendor, Some(Vendor::Kimi));
837 assert_eq!(cli.vendor.unwrap().to_id(), crate::vendor::VendorId::Kimi);
838 }
839
840 #[test]
841 fn vendor_grokbot_parses_to_grokbot_variant() {
842 let cli = Cli::parse_from(["ai-usagebar", "--vendor", "grokbot"]);
843 assert_eq!(cli.vendor, Some(Vendor::Grokbot));
844 assert_eq!(
845 cli.vendor.unwrap().to_id(),
846 crate::vendor::VendorId::Grokbot
847 );
848 assert_eq!(
850 id_to_vendor(crate::vendor::VendorId::Grokbot),
851 Vendor::Grokbot
852 );
853 }
854
855 #[test]
856 fn vendor_modelstudio_parses_to_modelstudio_variant() {
857 let cli = Cli::parse_from(["ai-usagebar", "--vendor", "modelstudio"]);
858 assert_eq!(cli.vendor, Some(Vendor::ModelStudio));
859 assert_eq!(
860 cli.vendor.unwrap().to_id(),
861 crate::vendor::VendorId::ModelStudio
862 );
863 assert_eq!(
864 id_to_vendor(crate::vendor::VendorId::ModelStudio),
865 Vendor::ModelStudio
866 );
867 }
868
869 #[test]
870 fn vendor_anthropic_api_uses_the_documented_slug() {
871 let cli = Cli::parse_from(["ai-usagebar", "--vendor", "anthropic_api"]);
872 assert_eq!(cli.vendor, Some(Vendor::AnthropicApi));
873 assert_eq!(
874 cli.vendor.unwrap().to_id(),
875 crate::vendor::VendorId::AnthropicApi
876 );
877 }
878
879 #[test]
880 fn every_vendor_parses_from_its_slug() {
881 for vendor in <Vendor as clap::ValueEnum>::value_variants() {
884 let slug = vendor.to_id().slug();
885 let cli = Cli::parse_from(["ai-usagebar", "--vendor", slug]);
886 assert_eq!(cli.vendor, Some(*vendor), "{slug}");
887 }
888 let legacy = Cli::parse_from(["ai-usagebar", "--vendor", "orca-router"]);
889 assert_eq!(legacy.vendor, Some(Vendor::OrcaRouter));
890 }
891
892 #[test]
893 fn disabled_kimi_primary_falls_back_to_an_enabled_vendor() {
894 let cli = Cli::parse_from(["ai-usagebar"]);
895 let mut cfg = crate::config::Config::default();
896 cfg.ui.primary = Some(crate::vendor::VendorId::Kimi);
897 assert_eq!(cli.resolve_vendor_with(&cfg, None), Vendor::Anthropic);
898 }
899
900 #[test]
901 fn explicit_kimi_remains_an_opt_in_override_when_disabled() {
902 let cli = Cli::parse_from(["ai-usagebar", "--vendor", "kimi"]);
903 assert_eq!(
904 cli.resolve_vendor_with(&crate::config::Config::default(), None),
905 Vendor::Kimi
906 );
907 }
908
909 #[test]
910 fn active_override_wins_over_config_primary_when_enabled() {
911 let cli = Cli::parse_from(["ai-usagebar"]);
914 let mut cfg = crate::config::Config::default();
915 cfg.ui.primary = Some(crate::vendor::VendorId::Openrouter);
916 let active = Some(crate::vendor::VendorId::Zai);
917 assert_eq!(cli.resolve_vendor_with(&cfg, active), Vendor::Zai);
918 }
919
920 #[test]
921 fn disabled_active_override_falls_back_to_config_primary() {
922 let cli = Cli::parse_from(["ai-usagebar"]);
925 let mut cfg = crate::config::Config::default();
926 cfg.zai.enabled = false;
927 cfg.ui.primary = Some(crate::vendor::VendorId::Openrouter);
928 let active = Some(crate::vendor::VendorId::Zai);
929 assert_eq!(cli.resolve_vendor_with(&cfg, active), Vendor::Openrouter);
930 }
931
932 #[test]
933 fn claudebar_compatible_flag_surface() {
934 let cli = Cli::parse_from([
935 "ai-usagebar",
936 "--icon",
937 "",
938 "--format",
939 "{session_pct}% · {session_reset}",
940 "--tooltip-format",
941 "S:{session_pct}",
942 "--pace-tolerance",
943 "10",
944 "--format-pace-color",
945 "--tooltip-pace-pts",
946 "--color-low",
947 "#50fa7b",
948 "--color-mid",
949 "#f1fa8c",
950 "--color-high",
951 "#ffb86c",
952 "--color-critical",
953 "#ff5555",
954 ]);
955 assert_eq!(cli.icon.as_deref(), Some(""));
956 assert_eq!(
957 cli.format.as_deref(),
958 Some("{session_pct}% · {session_reset}")
959 );
960 assert_eq!(cli.tooltip_format.as_deref(), Some("S:{session_pct}"));
961 assert_eq!(cli.pace_tolerance, 10);
962 assert!(cli.format_pace_color);
963 assert!(cli.tooltip_pace_pts);
964 assert_eq!(cli.color_low.as_deref(), Some("#50fa7b"));
965 assert_eq!(cli.color_critical.as_deref(), Some("#ff5555"));
966 }
967
968 #[test]
969 fn pretty_and_json_conflict() {
970 let res = Cli::try_parse_from(["ai-usagebar", "--pretty", "--json"]);
971 assert!(res.is_err());
972 }
973
974 #[test]
975 fn watch_disables_json_output() {
976 let cli = Cli::parse_from(["ai-usagebar", "--watch", "5"]);
977 assert_eq!(cli.watch, Some(5));
978 assert!(!cli.output_json());
979 }
980}