1use crate::historystore::store::HistoryStore;
2use crate::models::Role;
3use crate::models::{SessionView, TokenUsage};
4use crossterm::style::Stylize;
5use std::io::IsTerminal;
6use unicode_width::UnicodeWidthStr;
7
8pub const ANSI_REGEX_PATTERN: &str = r"\x1b\[[0-9;?]*[a-zA-Z]|\x1b].*?(\x1b\\|[\x07])";
9
10pub fn strip_ansi_codes(s: &str) -> String {
11 static RE: std::sync::LazyLock<regex::Regex> =
12 std::sync::LazyLock::new(|| regex::Regex::new(ANSI_REGEX_PATTERN).unwrap());
13 RE.replace_all(s, "").to_string()
14}
15
16pub fn get_terminal_width() -> usize {
17 static TERMINAL_WIDTH: std::sync::LazyLock<usize> = std::sync::LazyLock::new(|| {
18 if let Ok(w) = std::env::var("AICO_COLUMNS").map(|s| s.parse().unwrap_or(0))
20 && w > 0
21 {
22 return w;
23 }
24
25 if let Ok(w) = std::env::var("COLUMNS").map(|s| s.parse().unwrap_or(0))
27 && w > 0
28 {
29 return w;
30 }
31
32 if is_stdout_terminal()
34 && let Ok((w, _)) = crossterm::terminal::size()
35 {
36 return w as usize;
37 }
38
39 80
41 });
42
43 *TERMINAL_WIDTH
44}
45
46pub fn draw_panel(title: &str, lines: &[String], width: usize) {
47 let inner_width = width.saturating_sub(2);
48 let title_fmt = if !title.is_empty() {
49 format!(" {} ", title)
50 } else {
51 "".to_string()
52 };
53
54 let title_width = UnicodeWidthStr::width(title_fmt.as_str());
55 let total_dashes = inner_width.saturating_sub(title_width);
56 let left_dashes = total_dashes / 2;
57 let right_dashes = total_dashes - left_dashes;
58
59 println!(
60 "╭{}{}{}╮",
61 "─".repeat(left_dashes),
62 title_fmt,
63 "─".repeat(right_dashes)
64 );
65
66 for line in lines {
67 let stripped = strip_ansi_codes(line);
68 let visible_len = UnicodeWidthStr::width(stripped.as_str());
69 let total_padding = inner_width.saturating_sub(visible_len);
70 let left_padding = total_padding / 2;
71 let right_padding = total_padding - left_padding;
72
73 println!(
74 "│{}{}{}│",
75 " ".repeat(left_padding),
76 line,
77 " ".repeat(right_padding)
78 );
79 }
80
81 println!("╰{}╯", "─".repeat(inner_width));
82}
83
84pub fn is_stdout_terminal() -> bool {
85 if std::env::var("AICO_FORCE_TTY").is_ok() {
86 return true;
87 }
88 std::io::stdout().is_terminal()
89}
90
91pub fn is_stdin_terminal() -> bool {
92 std::io::stdin().is_terminal()
93}
94
95use crate::models::Mode;
96
97pub fn format_piped_output<'a>(
98 unified_diff: &'a Option<String>,
99 raw_content: &'a str,
100 mode: &Mode,
101) -> &'a str {
102 if matches!(mode, Mode::Diff) {
103 return unified_diff.as_deref().unwrap_or("");
104 }
105
106 if let Some(diff) = unified_diff
107 && !diff.is_empty()
108 {
109 return diff.as_str();
110 }
111
112 raw_content
113}
114
115pub fn format_tokens(n: u32) -> String {
116 if n >= 1000 {
117 format!("{:.1}k", n as f64 / 1000.0)
118 } else {
119 n.to_string()
120 }
121}
122
123pub fn format_thousands(n: u32) -> String {
124 let s = n.to_string();
125 let len = s.len();
126 let num_commas = (len.saturating_sub(1)) / 3;
127 let mut result = String::with_capacity(len + num_commas);
128
129 let offset = len % 3;
130 let mut chars = s.chars();
131
132 if offset > 0 {
133 for _ in 0..offset {
134 result.push(chars.next().unwrap());
135 }
136 }
137
138 for (i, c) in chars.enumerate() {
139 if i % 3 == 0 && (offset > 0 || i > 0) {
140 result.push(',');
141 }
142 result.push(c);
143 }
144 result
145}
146
147pub fn display_cost_summary(
148 usage: &TokenUsage,
149 current_cost: Option<f64>,
150 store: &HistoryStore,
151 view: &SessionView,
152) {
153 let mut prompt_info = format_tokens(usage.prompt_tokens);
154 if let Some(cached) = usage.cached_tokens
155 && cached > 0
156 {
157 prompt_info.push_str(&format!(" ({} cached)", format_tokens(cached)));
158 }
159
160 let mut completion_info = format_tokens(usage.completion_tokens);
161 if let Some(reasoning) = usage.reasoning_tokens
162 && reasoning > 0
163 {
164 completion_info.push_str(&format!(" ({} reasoning)", format_tokens(reasoning)));
165 }
166
167 let mut info = format!(
168 "Tokens: {} sent, {} received.",
169 prompt_info, completion_info
170 );
171
172 if let Some(cost) = current_cost {
173 let mut history_cost = 0.0;
174
175 if let Ok(records) = store.read_many(&view.message_indices) {
176 let start_idx = view.history_start_pair * 2;
177 for (i, record) in records.iter().enumerate() {
178 if i >= start_idx
179 && record.role == Role::Assistant
180 && let Some(c) = record.cost
181 {
182 history_cost += c;
183 }
184 }
185 }
186
187 let total_chat = history_cost + cost;
188 info.push_str(&format!(
189 " Cost: ${:.2}, current chat: ${:.2}",
190 cost, total_chat
191 ));
192 }
193
194 if is_stdout_terminal() {
195 eprintln!("{}", "---".dim());
196 eprintln!("{}", info.dim());
197 } else {
198 eprintln!("{}", info);
199 }
200}