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 bytes = s.as_bytes();
126
127 let mut groups: Vec<&str> = bytes
128 .rchunks(3)
129 .map(|chunk| std::str::from_utf8(chunk).unwrap()) .collect();
131
132 groups.reverse();
133
134 groups.join(",")
135}
136
137pub fn display_cost_summary(
138 usage: &TokenUsage,
139 current_cost: Option<f64>,
140 store: &HistoryStore,
141 view: &SessionView,
142) {
143 let mut prompt_info = format_tokens(usage.prompt_tokens);
144 if let Some(cached) = usage.cached_tokens
145 && cached > 0
146 {
147 prompt_info.push_str(&format!(" ({} cached)", format_tokens(cached)));
148 }
149
150 let mut completion_info = format_tokens(usage.completion_tokens);
151 if let Some(reasoning) = usage.reasoning_tokens
152 && reasoning > 0
153 {
154 completion_info.push_str(&format!(" ({} reasoning)", format_tokens(reasoning)));
155 }
156
157 let mut info = format!(
158 "Tokens: {} sent, {} received.",
159 prompt_info, completion_info
160 );
161
162 if let Some(cost) = current_cost {
163 let mut history_cost = 0.0;
164 let start_idx = view.history_start_pair * 2;
165
166 if start_idx < view.message_indices.len()
167 && let Ok(records) = store.read_many(&view.message_indices[start_idx..])
168 {
169 for record in records {
170 if record.role == Role::Assistant
171 && let Some(c) = record.cost
172 {
173 history_cost += c;
174 }
175 }
176 }
177
178 let total_chat = history_cost + cost;
179 info.push_str(&format!(
180 " Cost: ${:.2}, current chat: ${:.2}",
181 cost, total_chat
182 ));
183 }
184
185 if is_stdout_terminal() {
186 eprintln!("{}", "---".dim());
187 eprintln!("{}", info.dim());
188 } else {
189 eprintln!("{}", info);
190 }
191}