use crate::app::{App, Tab};
use crate::utils;
use chrono::Datelike;
impl App {
pub fn yank_to_clipboard(&mut self) {
let text = match self.active_tab {
Tab::Summary => self.build_summary_clipboard(),
Tab::Monthly => self.build_period_clipboard("Month"),
Tab::Yearly => self.build_period_clipboard("Year"),
Tab::LlmSummary => self.build_llm_clipboard(),
};
if let Ok(mut clipboard) = arboard::Clipboard::new() {
let _ = clipboard.set_text(&text);
}
}
fn build_llm_clipboard(&self) -> String {
if let Some(ref text) = self.llm.data {
let title = match self.llm.report_type {
crate::llm::LlmReportType::All => "LLM Report (All-Time)".to_string(),
crate::llm::LlmReportType::Monthly => format!(
"LLM Report ({}-{:02})",
self.llm.selected_month.year(),
self.llm.selected_month.month()
),
crate::llm::LlmReportType::Yearly => format!("LLM Report ({})", self.llm.selected_year),
};
format!("=== {} ===\n\n{}", title, text)
} else {
"No report generated yet.".to_string()
}
}
pub(crate) fn build_summary_clipboard(&self) -> String {
let (total_tokens, total_models, items, detail) = if let Some(ref data) = self.views.overview_view_data {
let models = data.model_tokens.len();
let mut items: Vec<(String, u64)> = data.project_tokens.iter()
.map(|(k, v)| (k.clone(), *v)).collect();
items.sort_by(|a, b| b.1.cmp(&a.1));
(data.total_tokens, models, items, Some(data.clone()))
} else {
let total = self.stats.model_usage.values().map(|m| m.total_tokens()).sum();
let models = self.stats.model_usage.len();
let mut items: Vec<(String, u64)> = self.project_dirs.iter()
.map(|p| (p.display_name.clone(), 0u64)).collect();
for s in &self.views.cached_sessions {
if let Some(idx) = items.iter().position(|n| n.0 == s.project_name) {
items[idx].1 += s.total_tokens;
}
}
items.sort_by(|a, b| b.1.cmp(&a.1));
(total, models, items, None)
};
let sessions = self.views.overview_view_data.as_ref().map(|d| d.total_sessions).unwrap_or(self.stats.total_sessions);
let messages = self.views.overview_view_data.as_ref().map(|d| d.total_messages).unwrap_or(self.stats.total_messages);
let mut out = String::new();
out.push_str(&format!("Projects: {}\n", self.project_dirs.len()));
out.push_str(&format!("Sessions: {}\n", sessions));
out.push_str(&format!("Messages: {}\n", messages));
out.push_str(&format!("Total Tokens: {}\n", utils::format_tokens(total_tokens)));
out.push_str(&format!("Models Used: {}\n", total_models));
out.push('\n');
if let Some(ref data) = detail {
out.push_str("Model\tTotal\tInput\tOutput\tCacheR\tCacheW\tCache%\tSearch\tFetch\n");
let mut models: Vec<(String, &crate::data::models::ModelTokenDetail)> =
data.model_token_detail.iter().map(|(k, v)| (k.clone(), v)).collect();
models.sort_by(|a, b| b.1.total_tokens.cmp(&a.1.total_tokens));
for (name, d) in &models {
out.push_str(&format!(
"{}\t{}\t{}\t{}\t{}\t{}\t{:.1}%\t{}\t{}\n",
name, utils::format_tokens(d.total_tokens), utils::format_tokens(d.input_tokens),
utils::format_tokens(d.output_tokens), utils::format_tokens(d.cache_read_input_tokens),
utils::format_tokens(d.cache_creation_input_tokens),
d.cache_hit_rate() * 100.0, d.web_search_requests, d.web_fetch_requests,
));
}
out.push('\n');
}
let total: u64 = items.iter().map(|(_, v)| *v).sum();
out.push_str("Project\tTokens\t%\n");
for (name, tokens) in &items {
let pct = if total > 0 { format!("{:.1}%", (*tokens as f64 / total as f64) * 100.0) } else { "0.0%".into() };
out.push_str(&format!("{}\t{}\t{}\n", name, utils::format_tokens(*tokens), pct));
}
out
}
pub(crate) fn build_period_clipboard(&self, _label: &str) -> String {
let data = match &self.views.current_view_data {
Some(d) => d, None => return "No data loaded.".to_string(),
};
let mut out = String::new();
out.push_str(&format!("Sessions: {}\n", data.total_sessions));
out.push_str(&format!("Messages: {}\n", data.total_messages));
out.push_str(&format!("Total Tokens: {}\n", utils::format_tokens(data.total_tokens)));
out.push_str(&format!("Models: {}\n", data.model_tokens.len()));
out.push_str(&format!("Projects: {}\n", data.project_tokens.len()));
out.push_str(&format!("Web Search: {}\n", data.total_web_search_requests));
out.push_str(&format!("Web Fetch: {}\n", data.total_web_fetch_requests));
out.push_str(&format!("Tool Calls: {}\n\n", data.total_tool_calls));
out.push_str("Model\tTotal\tInput\tOutput\tCacheR\tCacheW\tCache%\tSearch\tFetch\n");
let mut models: Vec<(String, &crate::data::models::ModelTokenDetail)> =
data.model_token_detail.iter().map(|(k, v)| (k.clone(), v)).collect();
models.sort_by(|a, b| b.1.total_tokens.cmp(&a.1.total_tokens));
for (name, d) in &models {
out.push_str(&format!(
"{}\t{}\t{}\t{}\t{}\t{}\t{:.1}%\t{}\t{}\n",
name, utils::format_tokens(d.total_tokens), utils::format_tokens(d.input_tokens),
utils::format_tokens(d.output_tokens), utils::format_tokens(d.cache_read_input_tokens),
utils::format_tokens(d.cache_creation_input_tokens),
d.cache_hit_rate() * 100.0, d.web_search_requests, d.web_fetch_requests,
));
}
out.push('\n');
let mut items: Vec<(String, u64)> = data.project_tokens.iter().map(|(k, v)| (k.clone(), *v)).collect();
items.sort_by(|a, b| b.1.cmp(&a.1));
let total: u64 = items.iter().map(|(_, v)| *v).sum();
out.push_str("Project\tTokens\t%\n");
for (name, tokens) in &items {
let pct = if total > 0 { format!("{:.1}%", (*tokens as f64 / total as f64) * 100.0) } else { "0.0%".into() };
out.push_str(&format!("{}\t{}\t{}\n", name, utils::format_tokens(*tokens), pct));
}
out
}
}