lean_ctx/core/stats/format/
views.rs1use super::util::{active_theme, day_total_saved, format_big, format_num, usd_estimate};
4use crate::core::theme;
5
6use super::super::model::CostModel;
7
8pub fn format_gain_graph() -> String {
10 let theme = active_theme();
11 let store = crate::core::stats::load();
12 let rst = theme::rst();
13 let bold = theme::bold();
14 let dim = theme::dim();
15
16 if store.daily.is_empty() {
17 return format!(
18 "{dim}No daily data yet.{rst} Use lean-ctx for a few days to see the graph."
19 );
20 }
21
22 let cm = CostModel::default();
23 let days: Vec<_> = store
24 .daily
25 .iter()
26 .rev()
27 .take(30)
28 .collect::<Vec<_>>()
29 .into_iter()
30 .rev()
31 .collect();
32
33 let savings: Vec<u64> = days.iter().map(|day| day_total_saved(day, &cm)).collect();
34
35 let max_saved = *savings.iter().max().unwrap_or(&1);
36 let max_saved = max_saved.max(1);
37
38 let bar_width = 36;
39 let mut out = Vec::new();
40
41 out.push(String::new());
42 out.push(format!(
43 " {icon} {title} {dim}Token Savings Graph (last 30 days){rst}",
44 icon = theme.header_icon(),
45 title = theme.brand_title(),
46 ));
47 out.push(format!(" {ln}", ln = theme.border_line(58)));
48 out.push(format!(
49 " {dim}{:>58}{rst}",
50 format!("peak: {}", format_big(max_saved))
51 ));
52 out.push(String::new());
53
54 for (i, day) in days.iter().enumerate() {
55 let saved = savings[i];
56 let ratio = saved as f64 / max_saved as f64;
57 let bar = theme::pad_right(&theme.gradient_bar(ratio, bar_width), bar_width);
58
59 let input_saved = day.input_tokens.saturating_sub(day.output_tokens);
60 let pct = if day.input_tokens > 0 {
61 input_saved as f64 / day.input_tokens as f64 * 100.0
62 } else {
63 0.0
64 };
65 let date_short = day.date.get(5..).unwrap_or(&day.date);
66
67 out.push(format!(
68 " {m}{date_short}{rst} {brd}│{rst} {bar} {bold}{:>6}{rst} {dim}{pct:.0}%{rst}",
69 format_big(saved),
70 m = theme.muted.fg(),
71 brd = theme.border.fg(),
72 ));
73 }
74
75 let total_saved: u64 = savings.iter().sum();
76 let total_cmds: u64 = days.iter().map(|day| day.commands).sum();
77 let spark = theme.gradient_sparkline(&savings);
78
79 out.push(String::new());
80 out.push(format!(" {ln}", ln = theme.border_line(58)));
81 out.push(format!(
82 " {spark} {bold}{txt}{}{rst} saved across {bold}{}{rst} commands",
83 format_big(total_saved),
84 format_num(total_cmds),
85 txt = theme.text.fg(),
86 ));
87 out.push(String::new());
88
89 out.join("\n")
90}
91
92#[allow(clippy::many_single_char_names)]
94pub fn format_gain_daily() -> String {
95 let theme = active_theme();
96 let store = crate::core::stats::load();
97 let rst = theme::rst();
98 let bold = theme::bold();
99 let dim = theme::dim();
100
101 if store.daily.is_empty() {
102 return format!("{dim}No daily data yet.{rst}");
103 }
104
105 let mut out = Vec::new();
106 let w = 76;
107
108 let side = theme.box_side();
109 let daily_box = |content: &str| -> String {
110 let padded = theme::pad_right(content, w);
111 format!(" {side}{padded}{side}")
112 };
113
114 out.push(String::new());
115 out.push(format!(
116 " {icon} {title} {dim}Daily Breakdown{rst}",
117 icon = theme.header_icon(),
118 title = theme.brand_title(),
119 ));
120 out.push(format!(" {}", theme.box_top(w)));
121 let hdr = format!(
122 " {bold}{txt}{:<12} {:>6} {:>10} {:>10} {:>7} {:>8} {:>8}{rst}",
123 "Date",
124 "Cmds",
125 "Input",
126 "Saved",
127 "Rate",
128 "USD",
129 "Ver",
130 txt = theme.text.fg(),
131 );
132 out.push(daily_box(&hdr));
133 out.push(format!(" {}", theme.box_mid(w)));
134
135 let days: Vec<_> = store
136 .daily
137 .iter()
138 .rev()
139 .take(30)
140 .collect::<Vec<_>>()
141 .into_iter()
142 .rev()
143 .cloned()
144 .collect();
145
146 let cm = CostModel::default();
147 for day in &days {
148 let saved = day_total_saved(day, &cm);
149 let input_saved = day.input_tokens.saturating_sub(day.output_tokens);
150 let pct = if day.input_tokens > 0 {
151 input_saved as f64 / day.input_tokens as f64 * 100.0
152 } else {
153 0.0
154 };
155 let pc = theme.pct_color(pct);
156 let usd = usd_estimate(saved);
157 let ver = if day.version.is_empty() {
158 "—".to_string()
159 } else {
160 format!("v{}", day.version)
161 };
162 let row = format!(
163 " {m}{:<12}{rst} {:>6} {:>10} {pc}{bold}{:>10}{rst} {pc}{:>6.1}%{rst} {dim}{:>8}{rst} {dim}{:>8}{rst}",
164 day.date,
165 day.commands,
166 format_big(day.input_tokens),
167 format_big(saved),
168 pct,
169 usd,
170 ver,
171 m = theme.muted.fg(),
172 );
173 out.push(daily_box(&row));
174 }
175
176 let total_input: u64 = store.daily.iter().map(|day| day.input_tokens).sum();
177 let total_saved: u64 = store
178 .daily
179 .iter()
180 .map(|day| day_total_saved(day, &cm))
181 .sum();
182 let total_pct = if total_input > 0 {
183 let input_saved: u64 = store
184 .daily
185 .iter()
186 .map(|day| day.input_tokens.saturating_sub(day.output_tokens))
187 .sum();
188 input_saved as f64 / total_input as f64 * 100.0
189 } else {
190 0.0
191 };
192 let total_usd = usd_estimate(total_saved);
193 let sc = theme.success.fg();
194
195 out.push(format!(" {}", theme.box_mid(w)));
196 let total_row = format!(
197 " {bold}{txt}{:<12}{rst} {:>6} {:>10} {sc}{bold}{:>10}{rst} {sc}{bold}{:>6.1}%{rst} {bold}{:>8}{rst} {bold}{:>8}{rst}",
198 "TOTAL",
199 format_num(store.total_commands),
200 format_big(total_input),
201 format_big(total_saved),
202 total_pct,
203 total_usd,
204 "",
205 txt = theme.text.fg(),
206 );
207 out.push(daily_box(&total_row));
208 out.push(format!(" {}", theme.box_bottom(w)));
209
210 let daily_savings: Vec<u64> = days.iter().map(|day| day_total_saved(day, &cm)).collect();
211 let spark = theme.gradient_sparkline(&daily_savings);
212 out.push(format!(" {dim}Trend:{rst} {spark}"));
213 out.push(String::new());
214
215 out.join("\n")
216}
217
218pub fn format_gain_json() -> String {
220 let store = crate::core::stats::load();
221 serde_json::to_string_pretty(&store).unwrap_or_else(|_| "{}".to_string())
222}