claude-dashboard 0.2.2

A terminal TUI dashboard that renders Claude Code usage reports with token stats, project breakdowns, and language distribution.
Documentation
//! Clipboard export (y key) — formats current view data as TSV.
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
    }
}