1mod api;
2pub mod cli;
3mod config;
4mod mcp;
5mod notify;
6pub mod ocuse;
9pub mod arkuse;
12mod paths;
13mod render;
14mod report_render;
15mod reports;
16mod snapshot;
17mod stores;
20mod update_check;
21
22#[cfg(test)]
23mod cli_tests;
24
25#[cfg(test)]
26mod config_tests;
27
28#[cfg(test)]
29mod mcp_tests;
30
31#[cfg(test)]
32mod render_tests;
33
34#[cfg(test)]
35mod main_tests;
36
37use std::io::{IsTerminal, Write};
38
39use crate::render::{BOLD, DIM, RESET};
40
41pub fn run() {
42 let args = cli::parse_args(cli::Binary::Cmduse);
43 if args.help {
44 cli::usage(cli::Binary::Cmduse);
45 return;
46 }
47
48 if let Some(cs) = args.config_set {
49 if let Err(e) = config::set(cli::Binary::Cmduse, &cs) {
50 eprintln!("error: {e}");
51 std::process::exit(1);
52 }
53 return;
54 }
55
56 if args.dismiss_update {
57 match update_check::latest_newer() {
58 Ok(Some(v)) => match config::set_dismissed(cli::Binary::Cmduse, &v) {
59 Ok(()) => println!("dismissed update notice for {v}"),
60 Err(e) => {
61 eprintln!("error: {e}");
62 std::process::exit(1);
63 }
64 },
65 Ok(None) => println!("cmduse is up to date ({})", env!("CARGO_PKG_VERSION")),
66 Err(e) => {
67 eprintln!("error: {e}");
68 std::process::exit(1);
69 }
70 }
71 return;
72 }
73
74 if let Some(cli::SubCmd::Mcp) = args.subcmd {
75 mcp::run();
76 return;
77 }
78
79 let fmt = output_fmt(&args);
80 let colors = !args.plain && render::color_enabled();
81 let tz = args.tz.unwrap_or(0);
82
83 match args.subcmd {
85 Some(cli::SubCmd::Daily) => {
86 match daily_output(args.last, tz, args.local, fmt) {
87 Ok(text) => print!("{text}"),
88 Err(e) => {
89 eprintln!("error: {e}");
90 std::process::exit(1);
91 }
92 }
93 return;
94 }
95 Some(cli::SubCmd::Hours) => {
96 match hourly_output(args.hours, tz, args.local, fmt) {
97 Ok(text) => print!("{text}"),
98 Err(e) => {
99 eprintln!("error: {e}");
100 std::process::exit(1);
101 }
102 }
103 return;
104 }
105 Some(cli::SubCmd::Model) => {
106 print!(
107 "{}",
108 match args.model_id.as_deref() {
109 Some(id) =>
110 model_detail_output(id, args.since.as_deref(), args.last, tz, fmt, colors),
111 None => model_output(
112 args.since.as_deref(),
113 args.last,
114 tz,
115 fmt,
116 colors,
117 rows_cap(&args, usize::MAX)
118 ),
119 }
120 );
121 return;
122 }
123 Some(cli::SubCmd::Session) => {
124 print!(
125 "{}",
126 session_output(
127 args.since.as_deref(),
128 args.last,
129 tz,
130 fmt,
131 colors,
132 rows_cap(&args, 20)
133 )
134 );
135 return;
136 }
137 Some(cli::SubCmd::Statusline) => {
138 statusline_cmd(&args);
139 return;
140 }
141 Some(cli::SubCmd::Models) => {
142 models_cmd(&args);
143 return;
144 }
145 Some(cli::SubCmd::Plans) => {
146 plans_cmd(&args);
147 return;
148 }
149 Some(cli::SubCmd::Mcp) => {}
151 None => {}
152 }
153
154 let cfg = config::load(cli::Binary::Cmduse);
155 let interval = args.interval.unwrap_or(cfg.interval_secs).clamp(1, 86_400);
156 let bar_width = args.bar_width.unwrap_or(cfg.bar_width).clamp(5, 200);
157 let burst_on = args.bursts.is_some() || cfg.burst_enabled;
160 let burst_cap = if burst_on {
161 args.bursts
162 .or(Some(cfg.burst_samples))
163 .unwrap_or(40)
164 .clamp(5, 240)
165 } else {
166 0
167 };
168 let notify_on_cap = cfg.notify_on_cap;
169
170 let update = update_check::check_sync(cfg.check_updates, cfg.dismissed_update.as_deref());
175
176 if args.once {
177 if let Some(msg) = &update {
178 eprintln!("{msg}");
179 }
180 let s = snapshot::snapshot(false, render::color_enabled());
181 if args.json {
182 println!("{}", render::render_json(&s));
183 } else if args.plain || !render::color_enabled() {
184 print!("{}", render::render_plain(&s));
185 } else {
186 print!("{}", render::render(&s, bar_width));
187 }
188 return;
189 }
190
191 let stdout = std::io::stdout();
200 let mut out = stdout.lock();
201 let mut prev_lines = 0usize;
202 let use_color = render::color_enabled();
205 let mut history: Vec<f64> = Vec::new();
208 let mut history_used: Vec<f64> = Vec::new(); let mut was_exceeded_5h = false;
211 let mut was_exceeded_wk = false;
212 let spinner = !args.plain && std::io::stdout().is_terminal();
215 loop {
216 let s = snapshot::snapshot(spinner, use_color && !args.plain);
217 let (rows, cols) = term_size().unwrap_or((0, 0));
218 let compact = cols > 0 && (rows < 16 || cols < 40);
221 let used = s
224 .credits
225 .window_limits
226 .five_hour
227 .as_ref()
228 .map(|w| w.used)
229 .unwrap_or(0.0);
230 let delta = history_used
231 .last()
232 .map(|prev| (used - prev).max(0.0))
233 .unwrap_or(0.0);
234 history_used.push(used);
235 if history_used.len() > 60 {
236 history_used.remove(0);
237 }
238 if burst_on {
239 history.push(delta);
240 if history.len() > burst_cap {
241 history.remove(0);
242 }
243 }
244 if notify_on_cap {
245 let ex5 = s
246 .credits
247 .window_limits
248 .five_hour
249 .as_ref()
250 .map(|w| w.exceeded)
251 .unwrap_or(false);
252 let exwk = s
253 .credits
254 .window_limits
255 .weekly
256 .as_ref()
257 .map(|w| w.exceeded)
258 .unwrap_or(false);
259 if ex5 && !was_exceeded_5h {
260 notify::cap(
261 "cmduse: limit exceeded",
262 "Command Code 5-hour window is over its cap",
263 );
264 }
265 if exwk && !was_exceeded_wk {
266 notify::cap(
267 "cmduse: limit exceeded",
268 "Command Code weekly window is over its cap",
269 );
270 }
271 was_exceeded_5h = ex5;
272 was_exceeded_wk = exwk;
273 }
274 let text = if compact {
275 compact_dashboard(
276 &s,
277 if burst_on { &history } else { &[] },
278 use_color && !args.plain,
279 )
280 } else if args.plain || !use_color {
281 render::render_plain(&s)
282 } else {
283 render::render(&s, bar_width)
284 };
285 let spark = if compact || !burst_on {
286 String::new()
287 } else if history.len() >= 2 && history.iter().any(|v| *v > 0.0) {
288 if use_color && !args.plain {
289 format!(
290 "{DIM}spend bursts ({}s samples){RESET} {}\n",
291 interval,
292 render::sparkline(&history)
293 )
294 } else {
295 format!(
296 "spend bursts ({}s samples) {}\n",
297 interval,
298 render::sparkline(&history)
299 )
300 }
301 } else {
302 String::new() };
304 let update_box = update
305 .as_ref()
306 .map(|m| render::update_box(m, (cols > 0).then_some(cols), use_color && !args.plain));
307 let status_line = if use_color && !args.plain {
308 format!("{DIM}refreshing every {interval}s · ctrl-c to quit{RESET}")
309 } else {
310 format!("refreshing every {interval}s · ctrl-c to quit")
311 };
312 let frame = match &update_box {
313 Some(u) => format!("{u}{text}{spark}{status_line}"),
314 None => format!("{text}{spark}{status_line}"),
315 };
316 prev_lines = redraw_frame(&mut out, &frame, prev_lines, (cols > 0).then_some(cols));
317 out.flush().ok();
318 for remaining in (1..interval).rev() {
320 std::thread::sleep(std::time::Duration::from_secs(1));
321 let msg = if use_color && !args.plain {
322 format!(
323 "\r\x1b[2K{DIM}refreshing every {interval}s · next refresh in {remaining}s · ctrl-c to quit{RESET}"
324 )
325 } else {
326 format!("\r\x1b[2Krefreshing every {interval}s · next refresh in {remaining}s · ctrl-c to quit")
327 };
328 write!(out, "{}", clip_to_width(&msg, (cols > 0).then_some(cols))).ok();
329 out.flush().ok();
330 }
331 }
332}
333
334fn compact_dashboard(s: &crate::render::Snapshot, history: &[f64], colors: bool) -> String {
340 use crate::render::plan_name;
341 if let Some(e) = &s.err {
342 if colors {
343 return format!("{BOLD}error{RESET} {e}\n");
344 } else {
345 return format!("error {e}\n");
346 }
347 }
348 let cap = cmduse_core::plan_monthly_cap(&s.sub.plan_id);
349 let cap_txt = cap.map(cmduse_core::money).unwrap_or_else(|| "-".into());
350 let rem_txt = cmduse_core::money(s.credits.credits.monthly_credits);
351 let h5 = s
352 .credits
353 .window_limits
354 .five_hour
355 .as_ref()
356 .map(|w| cmduse_core::money(w.used))
357 .unwrap_or_else(|| "-".into());
358 let status = if s.sub.status.is_empty() {
359 String::new()
360 } else {
361 format!(" · {}", s.sub.status)
362 };
363 let spark = if history.len() >= 2 && history.iter().any(|v| *v > 0.0) {
364 if colors {
365 format!(" {DIM}{}{RESET}", crate::render::sparkline(history))
366 } else {
367 format!(" {}", crate::render::sparkline(history))
368 }
369 } else {
370 String::new()
371 };
372 if colors {
373 format!(
374 "{BOLD}{}{RESET} {rem_txt}/{cap_txt} · 5h {h5}{status}{spark}\n",
375 plan_name(&s.sub.plan_id)
376 )
377 } else {
378 format!(
379 "{} {rem_txt}/{cap_txt} · 5h {h5}{status}{spark}\n",
380 plan_name(&s.sub.plan_id)
381 )
382 }
383}
384
385pub fn term_size() -> Option<(usize, usize)> {
390 let out = std::process::Command::new("sh")
391 .arg("-c")
392 .arg("stty size < /dev/tty")
393 .output()
394 .ok()?;
395 if !out.status.success() {
396 return None;
397 }
398 let text = String::from_utf8_lossy(&out.stdout);
399 let mut it = text.split_whitespace();
400 let rows = it.next()?.parse().ok()?;
401 let cols = it.next()?.parse().ok()?;
402 Some((rows, cols))
403}
404
405pub fn clip_to_width(line: &str, cols: Option<usize>) -> String {
409 let Some(cols) = cols else { return line.into() };
410 if cols == 0 {
411 return String::new();
412 }
413 let mut vis = 0usize;
414 let mut cs = line.chars().peekable();
415 while let Some(&c) = cs.peek() {
416 match c {
417 '\x1b' => {
418 cs.next();
419 for e in cs.by_ref() {
420 if e.is_ascii_alphabetic() {
421 break;
422 }
423 }
424 }
425 c if (c as u32) < 0x20 => {
426 cs.next(); }
428 _ => {
429 vis += 1;
430 cs.next();
431 }
432 }
433 }
434 if vis <= cols {
435 return line.into();
436 }
437 let mut out = String::new();
438 let mut kept = 0usize;
439 let mut cs = line.chars();
440 while let Some(c) = cs.next() {
441 match c {
442 '\x1b' => {
443 out.push(c);
444 for e in cs.by_ref() {
445 out.push(e);
446 if e.is_ascii_alphabetic() {
447 break;
448 }
449 }
450 }
451 '\r' => out.push(c), c if (c as u32) < 0x20 => {}
453 _ => {
454 if kept < cols {
455 out.push(c);
456 kept += 1;
457 }
458 }
459 }
460 }
461 if out.ends_with("\x1b[0m") {
462 out
463 } else {
464 out + "\x1b[0m"
465 }
466}
467
468pub fn redraw_frame(
472 out: &mut impl Write,
473 frame: &str,
474 prev_lines: usize,
475 cols: Option<usize>,
476) -> usize {
477 if prev_lines > 0 {
478 write!(out, "\x1b[{}F", prev_lines - 1).ok();
480 }
481 let lines: Vec<&str> = frame.lines().collect();
482 let n = lines.len();
483 for (i, line) in lines.iter().enumerate() {
484 let line = clip_to_width(line, cols);
485 if i + 1 < n {
486 write!(out, "\x1b[2K\r{line}\n").ok();
487 } else {
488 write!(out, "\x1b[2K\r{line}").ok();
490 }
491 }
492 if n < prev_lines {
493 write!(out, "\x1b[1B").ok();
501 for _ in (n + 1)..prev_lines {
502 write!(out, "\x1b[2K\x1b[1B").ok();
503 }
504 write!(out, "\x1b[2K").ok();
505 write!(out, "\x1b[{}F", prev_lines - n).ok();
506 }
507 n
508}
509
510fn models_cmd(args: &cli::Args) {
511 if args.gated {
512 if let Some(days) = cmduse_core::gate_age_days(cmduse_core::dates::now_secs()) {
513 if days > 30 {
514 eprintln!(
515 "warning: gating snapshot is {days}d old (CLI {}) — run `bun run extract` from the repo root",
516 cmduse_core::GATE_CLI_VERSION
517 );
518 }
519 }
520 }
521 let key = match api::api_key() {
522 Ok(k) => k,
523 Err(e) => {
524 eprintln!("error: {e}");
525 std::process::exit(1);
526 }
527 };
528 let (plan_id, unlocked) = if args.gated {
530 match (api::subscriptions(&key), api::credits(&key)) {
531 (Ok(s), Ok(c)) => (
532 s.plan_id,
533 c.credits.purchased_credits > 0.0 || c.credits.free_credits > 0.0,
534 ),
535 _ => (String::new(), true), }
537 } else {
538 (String::new(), true)
539 };
540 match api::models(&key) {
541 Ok(list) => {
542 if args.json {
543 for m in &list {
544 if args.gated {
545 let (allowed, reason) = cmduse_core::gate(&m.id, &plan_id, unlocked);
547 let line = serde_json::json!({
548 "id": m.id,
549 "name": m.name,
550 "context_length": m.context_length,
551 "owned_by": m.owned_by,
552 "allowed": allowed,
553 "reason": reason,
554 });
555 println!("{line}");
556 } else {
557 let line = serde_json::to_string(m).unwrap_or_else(|_| "{}".into());
558 println!("{line}");
559 }
560 }
561 return;
562 }
563 let list: Vec<_> = if args.gated {
564 list.into_iter()
565 .filter(|m| cmduse_core::gate_allowed(&m.id, &plan_id, unlocked))
566 .collect()
567 } else {
568 list
569 };
570 if list.is_empty() {
571 println!("no models returned");
572 return;
573 }
574 let mut rows: Vec<_> = list
575 .iter()
576 .map(|m| {
577 let ctx = m
578 .context_length
579 .map(|n| format!("{} ctx", render::compact(n)))
580 .unwrap_or_else(|| "-".into());
581 let name = m.name.as_deref().unwrap_or(&m.id);
582 (m.id.as_str(), name, ctx)
583 })
584 .collect();
585 rows.sort_by(|a, b| a.1.cmp(b.1));
586 let more = rows.len().saturating_sub(rows_cap(args, usize::MAX));
587 for (id, name, ctx) in rows.into_iter().take(rows_cap(args, usize::MAX)) {
588 println!("{name:<36} {ctx:<10} {id}");
589 }
590 if more > 0 {
591 println!("… {more} more (--rows N to widen)");
592 }
593 }
594 Err(e) => {
595 eprintln!("error: {e}");
596 std::process::exit(1);
597 }
598 }
599}
600
601fn plans_cmd(args: &cli::Args) {
602 let current = api::api_key()
604 .ok()
605 .and_then(|k| api::subscriptions(&k).ok())
606 .map(|s| s.plan_id)
607 .unwrap_or_default();
608 if args.json {
609 print!("{}", render::plans_json(¤t));
610 } else {
611 print!("{}", render::plans_table(¤t, render::color_enabled()));
612 }
613}
614
615pub(crate) fn usage_output() -> String {
620 render::render_plain(&snapshot::snapshot(false, render::color_enabled()))
621}
622
623pub(crate) fn plans_output(colors: bool) -> String {
626 let current = api::api_key()
627 .ok()
628 .and_then(|k| api::subscriptions(&k).ok())
629 .map(|s| s.plan_id)
630 .unwrap_or_default();
631 render::plans_table(¤t, colors)
632}
633
634pub(crate) fn models_output(gated: bool) -> Result<String, String> {
636 let key = api::api_key().map_err(|e| e.to_string())?;
637 let (plan_id, unlocked) = if gated {
638 match (api::subscriptions(&key), api::credits(&key)) {
639 (Ok(s), Ok(c)) => (
640 s.plan_id,
641 c.credits.purchased_credits > 0.0 || c.credits.free_credits > 0.0,
642 ),
643 _ => (String::new(), true), }
645 } else {
646 (String::new(), true)
647 };
648 let list = api::models(&key).map_err(|e| e.to_string())?;
649 let list: Vec<_> = if gated {
650 list.into_iter()
651 .filter(|m| cmduse_core::gate_allowed(&m.id, &plan_id, unlocked))
652 .collect()
653 } else {
654 list
655 };
656 if list.is_empty() {
657 return Ok("no models returned\n".into());
658 }
659 let mut rows: Vec<_> = list
660 .iter()
661 .map(|m| {
662 let ctx = m
663 .context_length
664 .map(|n| format!("{} ctx", render::compact(n)))
665 .unwrap_or_else(|| "-".into());
666 let name = m.name.as_deref().unwrap_or(&m.id);
667 (m.id.as_str(), name, ctx)
668 })
669 .collect();
670 rows.sort_by(|a, b| a.1.cmp(b.1));
671 let mut out = String::new();
672 for (id, name, ctx) in rows {
673 out.push_str(&format!("{name:<36} {ctx:<10} {id}\n"));
674 }
675 Ok(out)
676}
677
678pub(crate) fn daily_output(
680 days: Option<usize>,
681 tz: i64,
682 local: bool,
683 fmt: report_render::Fmt,
684) -> Result<String, String> {
685 let tz_suffix = |t: i64| cmduse_core::dates::tz_offset_suffix(t);
686 let data_source = if local {
688 None
689 } else {
690 api::api_key()
691 .ok()
692 .map(|k| reports::load_account_daily(days.unwrap_or(7), &k, tz))
693 };
694 match data_source {
695 Some(Ok(by_day)) => {
696 let total = reports::sum_days(&by_day);
697 Ok(report_render::table(
698 "account",
699 "all harnesses",
700 &by_day,
701 &total,
702 None,
703 fmt,
704 false,
705 ))
706 }
707 Some(Err(e)) => Err(e.to_string()),
708 None => {
709 let d = reports::load_local(tz);
710 let subtitle = if tz != 0 {
713 format!(
714 "offline, ~/.commandcode/projects + pi, dsh, UTC{}",
715 tz_suffix(tz)
716 )
717 } else {
718 "offline, ~/.commandcode/projects + pi, dsh".to_string()
719 };
720 Ok(report_render::table(
721 "local", &subtitle, &d.by_day, &d.total, days, fmt, false,
722 ))
723 }
724 }
725}
726
727pub(crate) fn hourly_output(
729 hours: Option<usize>,
730 tz: i64,
731 local: bool,
732 fmt: report_render::Fmt,
733) -> Result<String, String> {
734 let tz_suffix = |t: i64| cmduse_core::dates::tz_offset_suffix(t);
735 let rows = if local {
736 reports::load_local_hourly(hours.unwrap_or(24), tz)
737 } else {
738 let key = api::api_key().map_err(|e| e.to_string())?;
739 reports::load_account_hourly(hours.unwrap_or(24), &key, tz).map_err(|e| e.to_string())?
740 };
741 let subtitle = if local {
742 if tz != 0 {
743 format!("local, CLI sessions, UTC{}", tz_suffix(tz))
744 } else {
745 "local, CLI sessions".to_string()
746 }
747 } else if tz != 0 {
748 format!("all harnesses, UTC{}", tz_suffix(tz))
749 } else {
750 "all harnesses, UTC".to_string()
751 };
752 Ok(report_render::hourly_table(&rows, fmt, &subtitle, false))
753}
754
755fn local_since(since_iso: Option<&str>, days: Option<usize>) -> Option<f64> {
761 match since_iso {
762 Some(iso) => cmduse_core::dates::parse_iso_utc(iso),
763 None => {
764 days.map(|d| (cmduse_core::dates::now_secs() as f64 - (d as f64) * 86_400.0) * 1000.0)
765 }
766 }
767}
768
769pub(crate) fn model_output(
772 since_iso: Option<&str>,
773 days: Option<usize>,
774 tz: i64,
775 fmt: report_render::Fmt,
776 colors: bool,
777 rows: usize,
778) -> String {
779 let d = reports::load_local_since(tz, local_since(since_iso, days));
780 report_render::model_table(&d.by_model, fmt, colors, since_iso, rows)
781}
782
783pub(crate) fn model_detail_output(
786 id: &str,
787 since_iso: Option<&str>,
788 days: Option<usize>,
789 tz: i64,
790 fmt: report_render::Fmt,
791 colors: bool,
792) -> String {
793 let d = reports::load_local_since(tz, local_since(since_iso, days));
794 report_render::model_detail(&d.by_model, id, fmt, colors, since_iso)
795}
796
797pub(crate) fn session_output(
799 since_iso: Option<&str>,
800 days: Option<usize>,
801 tz: i64,
802 fmt: report_render::Fmt,
803 colors: bool,
804 rows: usize,
805) -> String {
806 let d = reports::load_local_since(tz, local_since(since_iso, days));
807 report_render::project_table(&d.by_project, fmt, colors, rows)
808}
809
810fn rows_cap(args: &cli::Args, default: usize) -> usize {
814 args.rows_cap(default)
815}
816
817fn output_fmt(args: &cli::Args) -> report_render::Fmt {
819 if args.json {
820 report_render::Fmt::Json
821 } else if args.csv {
822 report_render::Fmt::Csv
823 } else {
824 report_render::Fmt::Table
825 }
826}
827
828fn statusline_json(
834 plan: &str,
835 month_cap: f64,
836 month_remaining: f64,
837 five_hour: (f64, f64),
838 weekly: (f64, f64),
839) -> serde_json::Value {
840 let (h5u, h5c) = five_hour;
841 let (wku, wkc) = weekly;
842 serde_json::json!({
843 "plan": plan,
844 "monthSpent": (month_cap - month_remaining).max(0.0),
845 "monthCap": month_cap,
846 "fiveHourSpent": h5u,
847 "fiveHourCap": h5c,
848 "weeklySpent": wku,
849 "weeklyCap": wkc,
850 "monthlyRemaining": month_remaining,
851 "monthlyCap": month_cap,
852 "fiveHourUsed": h5u,
853 "weeklyUsed": wku,
854 })
855}
856
857fn statusline_cmd(args: &cli::Args) {
858 let cfg = config::load(cli::Binary::Cmduse);
859 let s = snapshot::snapshot(false, render::color_enabled());
860 let plan = render::plan_name(&s.sub.plan_id);
861 let cap = render::plan_monthly_cap(&s.sub.plan_id).unwrap_or(0.0);
862 if let Some(e) = &s.err {
863 println!("cmduse: {e}");
864 std::process::exit(1);
865 }
866 if args.json {
867 let w = |o: &Option<api::Window>| -> (f64, f64) {
868 (
869 o.as_ref().map(|w| w.used).unwrap_or(0.0),
870 o.as_ref().map(|w| w.cap).unwrap_or(0.0),
871 )
872 };
873 let out = statusline_json(
874 plan,
875 cap,
876 s.credits.credits.monthly_credits,
877 w(&s.credits.window_limits.five_hour),
878 w(&s.credits.window_limits.weekly),
879 );
880 print!("{out}");
881 return;
882 }
883 let h5 = s
884 .credits
885 .window_limits
886 .five_hour
887 .as_ref()
888 .map(|w| (w.used, w.cap));
889 let wk = s
890 .credits
891 .window_limits
892 .weekly
893 .as_ref()
894 .map(|w| (w.used, w.cap));
895 let now = s.now;
897 let eta_of = |w: &Option<api::Window>, dur: u64| -> Option<String> {
898 let w = w.as_ref()?;
899 let secs = cmduse_core::pace_eta(w.reset_at, dur, w.used, w.cap, now)?;
900 Some(format!(
901 "on pace to hit cap in {}",
902 cmduse_core::duration(secs as u64)
903 ))
904 };
905 let h5_eta = eta_of(
906 &s.credits.window_limits.five_hour,
907 cmduse_core::FIVE_HOUR_SECS,
908 );
909 let wk_eta = eta_of(&s.credits.window_limits.weekly, cmduse_core::WEEKLY_SECS);
910 let d = report_render::StatusData {
911 plan,
912 monthly_remaining: s.credits.credits.monthly_credits,
913 monthly_cap: cap,
914 five_hour: &h5,
915 weekly: &wk,
916 five_hour_eta: h5_eta,
917 weekly_eta: wk_eta,
918 bar_width: cfg.bar_width.min(30),
919 colors: cfg.statusline_colors,
920 ascii: cfg.statusline_ascii,
921 };
922 print!(
923 "{}",
924 report_render::render_statusline(&cfg.statusline_template, &d)
925 );
926}