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
341#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
342pub enum Vendor {
343 Anthropic,
344 #[value(name = "anthropic_api")]
345 AnthropicApi,
346 Openai,
347 Copilot,
348 Zai,
349 Openrouter,
350 Deepseek,
351 Kimi,
352 Kilo,
353 Novita,
354 Moonshot,
355 Grok,
356 Supergrok,
357 Grokbot,
358 Antigravity,
359 Cursor,
360 Minimax,
361 Kiro,
362 #[value(name = "nous")]
363 NousResearch,
364 #[value(name = "opencode-go")]
365 OpenCodeGo,
366 #[value(name = "commandcode")]
367 CommandCode,
368 Ollama,
369 #[value(name = "orcarouter", alias = "orca-router")]
372 OrcaRouter,
373 #[value(name = "modelstudio")]
374 ModelStudio,
375}
376
377impl Vendor {
378 pub fn to_id(self) -> crate::vendor::VendorId {
379 match self {
380 Vendor::Anthropic => crate::vendor::VendorId::Anthropic,
381 Vendor::AnthropicApi => crate::vendor::VendorId::AnthropicApi,
382 Vendor::Openai => crate::vendor::VendorId::Openai,
383 Vendor::Copilot => crate::vendor::VendorId::Copilot,
384 Vendor::Zai => crate::vendor::VendorId::Zai,
385 Vendor::Openrouter => crate::vendor::VendorId::Openrouter,
386 Vendor::Deepseek => crate::vendor::VendorId::Deepseek,
387 Vendor::Kimi => crate::vendor::VendorId::Kimi,
388 Vendor::Kilo => crate::vendor::VendorId::Kilo,
389 Vendor::Novita => crate::vendor::VendorId::Novita,
390 Vendor::Moonshot => crate::vendor::VendorId::Moonshot,
391 Vendor::Grok => crate::vendor::VendorId::Grok,
392 Vendor::Supergrok => crate::vendor::VendorId::Supergrok,
393 Vendor::Grokbot => crate::vendor::VendorId::Grokbot,
394 Vendor::Antigravity => crate::vendor::VendorId::Antigravity,
395 Vendor::Cursor => crate::vendor::VendorId::Cursor,
396 Vendor::Minimax => crate::vendor::VendorId::Minimax,
397 Vendor::Kiro => crate::vendor::VendorId::Kiro,
398 Vendor::NousResearch => crate::vendor::VendorId::NousResearch,
399 Vendor::OpenCodeGo => crate::vendor::VendorId::OpenCodeGo,
400 Vendor::CommandCode => crate::vendor::VendorId::CommandCode,
401 Vendor::Ollama => crate::vendor::VendorId::Ollama,
402 Vendor::OrcaRouter => crate::vendor::VendorId::OrcaRouter,
403 Vendor::ModelStudio => crate::vendor::VendorId::ModelStudio,
404 }
405 }
406}
407
408impl Cli {
409 pub fn has_explicit_vendor(&self) -> bool {
411 self.vendor.is_some()
412 }
413
414 pub fn resolved_vendor(&self, config: &crate::config::Config) -> Vendor {
425 let active = if self.has_explicit_vendor() {
431 None
432 } else {
433 crate::active::read()
434 };
435 self.resolve_vendor_with(config, active)
436 }
437
438 pub fn resolve_vendor_with(
443 &self,
444 config: &crate::config::Config,
445 active: Option<crate::vendor::VendorId>,
446 ) -> Vendor {
447 if let Some(v) = self.vendor {
448 return v;
449 }
450 if let Some(id) = active
451 && config.is_enabled(id)
452 {
453 return id_to_vendor(id);
454 }
455 if let Some(id) = config.ui.primary
456 && config.is_enabled(id)
457 {
458 return id_to_vendor(id);
459 }
460 if config.is_enabled(crate::vendor::VendorId::Anthropic) {
461 return Vendor::Anthropic;
462 }
463 config
464 .enabled_vendors()
465 .into_iter()
466 .next()
467 .map(id_to_vendor)
468 .unwrap_or(Vendor::Anthropic)
471 }
472}
473
474fn id_to_vendor(id: crate::vendor::VendorId) -> Vendor {
475 match id {
476 crate::vendor::VendorId::Anthropic => Vendor::Anthropic,
477 crate::vendor::VendorId::AnthropicApi => Vendor::AnthropicApi,
478 crate::vendor::VendorId::Openai => Vendor::Openai,
479 crate::vendor::VendorId::Copilot => Vendor::Copilot,
480 crate::vendor::VendorId::Zai => Vendor::Zai,
481 crate::vendor::VendorId::Openrouter => Vendor::Openrouter,
482 crate::vendor::VendorId::Deepseek => Vendor::Deepseek,
483 crate::vendor::VendorId::Kimi => Vendor::Kimi,
484 crate::vendor::VendorId::Kilo => Vendor::Kilo,
485 crate::vendor::VendorId::Novita => Vendor::Novita,
486 crate::vendor::VendorId::Moonshot => Vendor::Moonshot,
487 crate::vendor::VendorId::Grok => Vendor::Grok,
488 crate::vendor::VendorId::Supergrok => Vendor::Supergrok,
489 crate::vendor::VendorId::Grokbot => Vendor::Grokbot,
490 crate::vendor::VendorId::Antigravity => Vendor::Antigravity,
491 crate::vendor::VendorId::Cursor => Vendor::Cursor,
492 crate::vendor::VendorId::Minimax => Vendor::Minimax,
493 crate::vendor::VendorId::Kiro => Vendor::Kiro,
494 crate::vendor::VendorId::NousResearch => Vendor::NousResearch,
495 crate::vendor::VendorId::OpenCodeGo => Vendor::OpenCodeGo,
496 crate::vendor::VendorId::CommandCode => Vendor::CommandCode,
497 crate::vendor::VendorId::Ollama => Vendor::Ollama,
498 crate::vendor::VendorId::OrcaRouter => Vendor::OrcaRouter,
499 crate::vendor::VendorId::ModelStudio => Vendor::ModelStudio,
500 }
501}
502
503impl Cli {
504 pub fn output_json(&self) -> bool {
507 if self.json {
508 return true;
509 }
510 if self.pretty || self.watch.is_some() {
511 return false;
512 }
513 !is_stdout_tty()
515 }
516}
517
518fn is_stdout_tty() -> bool {
519 use std::io::IsTerminal;
520 std::io::stdout().is_terminal()
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526 use clap::{Parser, error::ErrorKind};
527
528 #[test]
529 fn settings_enable_requires_a_known_vendor() {
530 assert!(matches!(
531 Cli::try_parse_from(["ai-usagebar", "settings", "enable", "anthropic"])
532 .unwrap()
533 .command,
534 Some(Command::Settings {
535 action: SettingsAction::Enable {
536 vendor: Vendor::Anthropic
537 }
538 })
539 ));
540 assert!(Cli::try_parse_from(["ai-usagebar", "settings", "enable", "unknown"]).is_err());
541 assert!(Cli::try_parse_from(["ai-usagebar", "settings", "enable"]).is_err());
542 }
543
544 #[test]
545 fn version_flags_report_the_crate_version() {
546 let expected = format!("ai-usagebar {}\n", env!("CARGO_PKG_VERSION"));
547
548 for flag in ["--version", "-V"] {
549 let err = Cli::try_parse_from(["ai-usagebar", flag])
550 .expect_err("a version flag exits through clap's display path");
551 assert_eq!(err.kind(), ErrorKind::DisplayVersion, "flag: {flag}");
552 assert_eq!(err.to_string(), expected, "flag: {flag}");
553 }
554 }
555
556 #[test]
557 fn usage_subcommand_parses_machine_readable_mode() {
558 let cli = Cli::parse_from(["ai-usagebar", "usage", "--json"]);
559 assert!(matches!(cli.command, Some(Command::Usage { json: true })));
560 }
561
562 #[test]
563 fn usage_help_states_complete_document_exits_zero() {
564 let err = Cli::try_parse_from(["ai-usagebar", "usage", "--help"])
565 .expect_err("help exits through clap's display path");
566 assert_eq!(err.kind(), ErrorKind::DisplayHelp);
567 let help = err.to_string();
568 assert!(
569 help.contains("Exits 0 after printing a complete document"),
570 "{help}"
571 );
572 assert!(
573 help.contains("even when every entry") && help.contains("error"),
574 "{help}"
575 );
576 assert!(
577 help.contains("Non-zero only when the document cannot be produced"),
578 "{help}"
579 );
580 }
581
582 #[test]
583 fn detect_subcommand_parses_its_flags_and_takes_no_widget_flags() {
584 let bare = Cli::parse_from(["ai-usagebar", "detect"]);
585 assert!(matches!(
586 bare.command,
587 Some(Command::Detect {
588 all: false,
589 json: false
590 })
591 ));
592
593 let full = Cli::parse_from(["ai-usagebar", "detect", "--all", "--json"]);
594 assert!(matches!(
595 full.command,
596 Some(Command::Detect {
597 all: true,
598 json: true
599 })
600 ));
601
602 assert!(Cli::try_parse_from(["ai-usagebar", "--vendor", "kimi", "detect"]).is_err());
603 }
604
605 #[test]
606 fn new_vendor_values_and_auth_commands_parse_exactly() {
607 let nous = Cli::parse_from(["ai-usagebar", "--vendor", "nous"]);
608 assert_eq!(nous.vendor, Some(Vendor::NousResearch));
609 let opencode = Cli::parse_from(["ai-usagebar", "--vendor", "opencode-go"]);
610 assert_eq!(opencode.vendor, Some(Vendor::OpenCodeGo));
611 let copilot = Cli::parse_from(["ai-usagebar", "--vendor", "copilot"]);
612 assert_eq!(copilot.vendor, Some(Vendor::Copilot));
613 let login = Cli::parse_from(["ai-usagebar", "auth", "nous", "login"]);
614 assert!(matches!(login.command, Some(Command::Auth { .. })));
615 }
616
617 #[test]
618 fn settings_subcommands_are_additive_and_take_no_widget_flags() {
619 let show = Cli::parse_from(["ai-usagebar", "settings", "show"]);
620 assert!(matches!(
621 show.command,
622 Some(Command::Settings {
623 action: SettingsAction::Show
624 })
625 ));
626
627 let apply = Cli::parse_from(["ai-usagebar", "settings", "apply"]);
628 assert!(matches!(
629 apply.command,
630 Some(Command::Settings {
631 action: SettingsAction::Apply
632 })
633 ));
634
635 assert!(
636 Cli::try_parse_from(["ai-usagebar", "--vendor", "kimi", "settings", "show",]).is_err()
637 );
638 }
639
640 #[test]
641 fn defaults_match_claudebar() {
642 let cli = Cli::parse_from(["ai-usagebar"]);
643 assert_eq!(cli.vendor, None);
644 let cfg = crate::config::Config::default();
649 assert_eq!(cli.resolve_vendor_with(&cfg, None), Vendor::Anthropic);
650 assert_eq!(cli.pace_tolerance, 5);
651 assert!(cli.format.is_none());
652 assert!(cli.tooltip_format.is_none());
653 assert!(cli.icon.is_none());
654 assert!(!cli.format_pace_color);
655 assert!(!cli.tooltip_pace_pts);
656 assert!(!cli.pretty);
657 assert!(!cli.json);
658 assert!(cli.watch.is_none());
659 assert!(cli.command.is_none());
660 }
661
662 #[test]
663 fn account_add_subcommand_parses_without_widget_flags() {
664 let cli = Cli::parse_from(["ai-usagebar", "account", "add", "work", "--no-login"]);
665 assert!(matches!(
666 cli.command,
667 Some(Command::Account {
668 action: AccountAction::Add {
669 ref label,
670 no_login: true,
671 desktop: false,
672 ..
673 }
674 }) if label == "work"
675 ));
676 }
677
678 #[test]
681 fn account_add_desktop_takes_an_email_and_rejects_no_login() {
682 let cli = Cli::parse_from([
683 "ai-usagebar",
684 "account",
685 "add",
686 "work",
687 "--desktop",
688 "--email",
689 "a@b.test",
690 "-y",
691 ]);
692 assert!(matches!(
693 cli.command,
694 Some(Command::Account {
695 action: AccountAction::Add {
696 desktop: true,
697 yes: true,
698 email: Some(ref email),
699 ..
700 }
701 }) if email == "a@b.test"
702 ));
703 assert!(
704 Cli::try_parse_from([
705 "ai-usagebar",
706 "account",
707 "add",
708 "w",
709 "--desktop",
710 "--no-login"
711 ])
712 .is_err()
713 );
714 assert!(
716 Cli::try_parse_from(["ai-usagebar", "account", "add", "w", "--email", "a@b.test"])
717 .is_err()
718 );
719 }
720
721 #[test]
722 fn account_switch_defaults_to_both_surfaces() {
723 let cli = Cli::parse_from(["ai-usagebar", "account", "switch", "work", "--dry-run"]);
724 assert!(matches!(
725 cli.command,
726 Some(Command::Account {
727 action: AccountAction::Switch {
728 ref label,
729 desktop: false,
730 cli: false,
731 dry_run: true,
732 keep_backups: 10,
733 ..
734 }
735 }) if label == "work"
736 ));
737 }
738
739 #[test]
740 fn account_subcommand_rejects_ignored_widget_flags() {
741 assert!(
742 Cli::try_parse_from([
743 "ai-usagebar",
744 "--vendor",
745 "anthropic",
746 "account",
747 "add",
748 "work",
749 ])
750 .is_err()
751 );
752 }
753
754 #[test]
755 fn multi_account_flags_are_stable_api() {
756 let cli = Cli::parse_from([
760 "ai-usagebar",
761 "--vendor",
762 "anthropic",
763 "--cache-dir",
764 "/tmp/acct-a",
765 "--creds-path",
766 "/tmp/acct-a/credentials.json",
767 ]);
768 assert_eq!(
769 cli.cache_dir.as_deref(),
770 Some(std::path::Path::new("/tmp/acct-a"))
771 );
772 assert_eq!(
773 cli.creds_path.as_deref(),
774 Some(std::path::Path::new("/tmp/acct-a/credentials.json"))
775 );
776 }
777
778 #[test]
779 fn primary_from_config_wins_when_vendor_unset() {
780 let cli = Cli::parse_from(["ai-usagebar"]);
782 let mut cfg = crate::config::Config::default();
783 cfg.ui.primary = Some(crate::vendor::VendorId::Openrouter);
784 assert_eq!(cli.resolve_vendor_with(&cfg, None), Vendor::Openrouter);
785 }
786
787 #[test]
788 fn explicit_vendor_overrides_everything() {
789 let cli = Cli::parse_from(["ai-usagebar", "--vendor", "zai"]);
792 let mut cfg = crate::config::Config::default();
793 cfg.ui.primary = Some(crate::vendor::VendorId::Openrouter);
794 let active = Some(crate::vendor::VendorId::Openai);
795 assert_eq!(cli.resolve_vendor_with(&cfg, active), Vendor::Zai);
796 }
797
798 #[test]
799 fn vendor_kimi_parses_to_kimi_variant() {
800 let cli = Cli::parse_from(["ai-usagebar", "--vendor", "kimi"]);
801 assert_eq!(cli.vendor, Some(Vendor::Kimi));
802 assert_eq!(cli.vendor.unwrap().to_id(), crate::vendor::VendorId::Kimi);
803 }
804
805 #[test]
806 fn vendor_grokbot_parses_to_grokbot_variant() {
807 let cli = Cli::parse_from(["ai-usagebar", "--vendor", "grokbot"]);
808 assert_eq!(cli.vendor, Some(Vendor::Grokbot));
809 assert_eq!(
810 cli.vendor.unwrap().to_id(),
811 crate::vendor::VendorId::Grokbot
812 );
813 assert_eq!(
815 id_to_vendor(crate::vendor::VendorId::Grokbot),
816 Vendor::Grokbot
817 );
818 }
819
820 #[test]
821 fn vendor_modelstudio_parses_to_modelstudio_variant() {
822 let cli = Cli::parse_from(["ai-usagebar", "--vendor", "modelstudio"]);
823 assert_eq!(cli.vendor, Some(Vendor::ModelStudio));
824 assert_eq!(
825 cli.vendor.unwrap().to_id(),
826 crate::vendor::VendorId::ModelStudio
827 );
828 assert_eq!(
829 id_to_vendor(crate::vendor::VendorId::ModelStudio),
830 Vendor::ModelStudio
831 );
832 }
833
834 #[test]
835 fn vendor_anthropic_api_uses_the_documented_slug() {
836 let cli = Cli::parse_from(["ai-usagebar", "--vendor", "anthropic_api"]);
837 assert_eq!(cli.vendor, Some(Vendor::AnthropicApi));
838 assert_eq!(
839 cli.vendor.unwrap().to_id(),
840 crate::vendor::VendorId::AnthropicApi
841 );
842 }
843
844 #[test]
845 fn every_vendor_parses_from_its_slug() {
846 for vendor in <Vendor as clap::ValueEnum>::value_variants() {
849 let slug = vendor.to_id().slug();
850 let cli = Cli::parse_from(["ai-usagebar", "--vendor", slug]);
851 assert_eq!(cli.vendor, Some(*vendor), "{slug}");
852 }
853 let legacy = Cli::parse_from(["ai-usagebar", "--vendor", "orca-router"]);
854 assert_eq!(legacy.vendor, Some(Vendor::OrcaRouter));
855 }
856
857 #[test]
858 fn disabled_kimi_primary_falls_back_to_an_enabled_vendor() {
859 let cli = Cli::parse_from(["ai-usagebar"]);
860 let mut cfg = crate::config::Config::default();
861 cfg.ui.primary = Some(crate::vendor::VendorId::Kimi);
862 assert_eq!(cli.resolve_vendor_with(&cfg, None), Vendor::Anthropic);
863 }
864
865 #[test]
866 fn explicit_kimi_remains_an_opt_in_override_when_disabled() {
867 let cli = Cli::parse_from(["ai-usagebar", "--vendor", "kimi"]);
868 assert_eq!(
869 cli.resolve_vendor_with(&crate::config::Config::default(), None),
870 Vendor::Kimi
871 );
872 }
873
874 #[test]
875 fn active_override_wins_over_config_primary_when_enabled() {
876 let cli = Cli::parse_from(["ai-usagebar"]);
879 let mut cfg = crate::config::Config::default();
880 cfg.ui.primary = Some(crate::vendor::VendorId::Openrouter);
881 let active = Some(crate::vendor::VendorId::Zai);
882 assert_eq!(cli.resolve_vendor_with(&cfg, active), Vendor::Zai);
883 }
884
885 #[test]
886 fn disabled_active_override_falls_back_to_config_primary() {
887 let cli = Cli::parse_from(["ai-usagebar"]);
890 let mut cfg = crate::config::Config::default();
891 cfg.zai.enabled = false;
892 cfg.ui.primary = Some(crate::vendor::VendorId::Openrouter);
893 let active = Some(crate::vendor::VendorId::Zai);
894 assert_eq!(cli.resolve_vendor_with(&cfg, active), Vendor::Openrouter);
895 }
896
897 #[test]
898 fn claudebar_compatible_flag_surface() {
899 let cli = Cli::parse_from([
900 "ai-usagebar",
901 "--icon",
902 "",
903 "--format",
904 "{session_pct}% · {session_reset}",
905 "--tooltip-format",
906 "S:{session_pct}",
907 "--pace-tolerance",
908 "10",
909 "--format-pace-color",
910 "--tooltip-pace-pts",
911 "--color-low",
912 "#50fa7b",
913 "--color-mid",
914 "#f1fa8c",
915 "--color-high",
916 "#ffb86c",
917 "--color-critical",
918 "#ff5555",
919 ]);
920 assert_eq!(cli.icon.as_deref(), Some(""));
921 assert_eq!(
922 cli.format.as_deref(),
923 Some("{session_pct}% · {session_reset}")
924 );
925 assert_eq!(cli.tooltip_format.as_deref(), Some("S:{session_pct}"));
926 assert_eq!(cli.pace_tolerance, 10);
927 assert!(cli.format_pace_color);
928 assert!(cli.tooltip_pace_pts);
929 assert_eq!(cli.color_low.as_deref(), Some("#50fa7b"));
930 assert_eq!(cli.color_critical.as_deref(), Some("#ff5555"));
931 }
932
933 #[test]
934 fn pretty_and_json_conflict() {
935 let res = Cli::try_parse_from(["ai-usagebar", "--pretty", "--json"]);
936 assert!(res.is_err());
937 }
938
939 #[test]
940 fn watch_disables_json_output() {
941 let cli = Cli::parse_from(["ai-usagebar", "--watch", "5"]);
942 assert_eq!(cli.watch, Some(5));
943 assert!(!cli.output_json());
944 }
945}