hackatime 0.2.0

Terminal CLI for viewing Hackatime stats with OAuth login
use serde::Deserialize;
use serde_json::Value;

#[derive(Debug, Clone, Deserialize)]
pub struct UserProfile {
    pub id: Option<u64>,
    pub github_username: Option<String>,
    pub emails: Option<Vec<String>>,
}

impl UserProfile {
    pub fn display_name(&self) -> String {
        self.github_username
            .clone()
            .or_else(|| {
                self.emails
                    .as_ref()
                    .and_then(|emails| emails.first().cloned())
                    .and_then(|email| email.split('@').next().map(ToOwned::to_owned))
            })
            .unwrap_or_else(|| "hackatime".to_string())
    }
}

#[derive(Debug, Clone, Deserialize)]
pub struct DurationResponse {
    pub total_seconds: Option<f64>,
    pub text: Option<String>,
    pub human_readable_total: Option<String>,
    #[serde(flatten)]
    pub extra: Value,
}

impl DurationResponse {
    pub fn display(&self) -> String {
        self.text
            .clone()
            .or_else(|| self.human_readable_total.clone())
            .or_else(|| {
                self.total_seconds.map(|seconds| {
                    let hours = seconds / 3600.0;
                    format!("{hours:.1} hrs")
                })
            })
            .or_else(|| {
                self.extra
                    .get("total")
                    .and_then(Value::as_str)
                    .map(ToOwned::to_owned)
            })
            .unwrap_or_else(|| "Unavailable".to_string())
    }
}

#[derive(Debug, Clone, Deserialize)]
pub struct Heartbeat {
    pub project: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct StreakResponse {
    pub streak_days: Option<u64>,
}

impl StreakResponse {
    pub fn display(&self) -> String {
        self.streak_days
            .map(|days| format!("{days} days"))
            .unwrap_or_else(|| "Unavailable".to_string())
    }
}

#[derive(Debug, Clone, Deserialize)]
pub struct ProjectSummary {
    pub name: Option<String>,
    pub text: Option<String>,
    pub total_seconds: Option<f64>,
}

impl ProjectSummary {
    pub fn display_time(&self) -> String {
        self.text
            .clone()
            .or_else(|| {
                self.total_seconds.map(|seconds| {
                    let hours = seconds / 3600.0;
                    format!("{hours:.1} hrs")
                })
            })
            .unwrap_or_else(|| "Unavailable".to_string())
    }
}

#[derive(Debug, Clone, Deserialize)]
pub struct ProjectsResponse {
    pub projects: Vec<ProjectSummary>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct UserStatsResponse {
    pub data: UserStatsSummary,
}

#[derive(Debug, Clone, Deserialize)]
pub struct UserStatsSummary {
    pub languages: Option<Vec<LanguageStat>>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct LanguageStat {
    pub name: Option<String>,
    pub total_seconds: Option<f64>,
    pub percent: Option<f64>,
}

#[derive(Debug, Clone)]
pub struct DashboardData {
    pub title: String,
    pub layout: DashboardLayout,
    pub stats: Vec<StatLine>,
    pub languages_title: Option<String>,
    pub languages: Vec<LanguageLine>,
}

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum DashboardLayout {
    Standard,
    Fetch,
}

#[derive(Debug, Clone)]
pub struct StatLine {
    pub label: String,
    pub value: String,
}

#[derive(Debug, Clone)]
pub struct LanguageLine {
    pub name: String,
    pub percent: f64,
    pub hours_text: String,
}