horae 0.2.5

Capture a task in under 10 seconds. GTD terminal manager (ratatui TUI + CLI) with ~2 ms startup, reactive completions, recurring habits, pomodoro, and phone sync. Rust + SQLite, fully offline.
use anyhow::{Context, Result};
use chrono::{Datelike, Local, NaiveDate, Weekday};
use horae_core::lunar::{self, CalendarDayInfo};
use serde::Serialize;

#[derive(Serialize)]
struct CalendarJson {
    date: String,
    weekday: String,
    lunar: LunarJson,
    solar_term: Option<String>,
    holidays: Vec<HolidayJson>,
    next_solar_term: NextSolarTermJson,
    next_major_holiday: Option<NextMajorHolidayJson>,
    warnings: Vec<WarningJson>,
    summary: String,
}

#[derive(Serialize)]
struct LunarJson {
    year: i32,
    month: u32,
    day: u32,
    is_leap: bool,
    ganzhi_year: String,
    zodiac: String,
    month_name: String,
    day_name: String,
    formatted: String,
}

#[derive(Serialize)]
struct HolidayJson {
    name: String,
    is_major: bool,
    hint: String,
}

#[derive(Serialize)]
struct NextSolarTermJson {
    name: String,
    date: String,
    days_left: i64,
}

#[derive(Serialize)]
struct NextMajorHolidayJson {
    name: String,
    date: String,
    days_left: i64,
    hint: String,
}

#[derive(Serialize)]
struct WarningJson {
    holiday_name: String,
    days_left: i64,
    target_date: String,
    hint: String,
    message: String,
}

fn weekday_cn(w: Weekday) -> &'static str {
    match w {
        Weekday::Mon => "星期一",
        Weekday::Tue => "星期二",
        Weekday::Wed => "星期三",
        Weekday::Thu => "星期四",
        Weekday::Fri => "星期五",
        Weekday::Sat => "星期六",
        Weekday::Sun => "星期日",
    }
}

pub fn run(date_str: Option<&str>, short: bool, json: bool) -> Result<()> {
    let target_date = if let Some(s) = date_str {
        if let Ok(d) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
            d
        } else if let Ok(ms) = horae_core::time::parse_time(s) {
            chrono::DateTime::from_timestamp_millis(ms)
                .map(|dt| dt.with_timezone(&Local).naive_local().date())
                .context("invalid timestamp")?
        } else {
            anyhow::bail!(
                "无法解析日期: '{}',请使用 YYYY-MM-DD 格式 (例如 2026-09-04)",
                s
            );
        }
    } else {
        Local::now().naive_local().date()
    };

    let cal = lunar::day_calendar_info(target_date)
        .with_context(|| format!("日期 {} 超出支持的农历年份范围 (1900-2100)", target_date))?;

    if short {
        println!("{}", cal.status_line());
        return Ok(());
    }

    if json {
        let (nst, nst_date, nst_days) = cal.next_solar_term;
        let nmh = cal
            .next_major_holiday
            .as_ref()
            .map(|(h, d, days)| NextMajorHolidayJson {
                name: h.name.clone(),
                date: d.format("%Y-%m-%d").to_string(),
                days_left: *days,
                hint: h.hint.clone(),
            });
        let warnings = cal
            .warnings
            .iter()
            .map(|w| WarningJson {
                holiday_name: w.holiday_name.clone(),
                days_left: w.days_left,
                target_date: w.target_date.format("%Y-%m-%d").to_string(),
                hint: w.hint.clone(),
                message: w.message(),
            })
            .collect();
        let holidays = cal
            .holidays
            .iter()
            .map(|h| HolidayJson {
                name: h.name.clone(),
                is_major: h.is_major,
                hint: h.hint.clone(),
            })
            .collect();

        let data = CalendarJson {
            date: target_date.format("%Y-%m-%d").to_string(),
            weekday: weekday_cn(target_date.weekday()).to_string(),
            lunar: LunarJson {
                year: cal.lunar.year,
                month: cal.lunar.month,
                day: cal.lunar.day,
                is_leap: cal.lunar.is_leap,
                ganzhi_year: cal.lunar.ganzhi_year.to_string(),
                zodiac: cal.lunar.zodiac.to_string(),
                month_name: cal.lunar.month_name.to_string(),
                day_name: cal.lunar.day_name.to_string(),
                formatted: cal.lunar.to_string(),
            },
            solar_term: cal.solar_term.map(|st| st.name().to_string()),
            holidays,
            next_solar_term: NextSolarTermJson {
                name: nst.name().to_string(),
                date: nst_date.format("%Y-%m-%d").to_string(),
                days_left: nst_days,
            },
            next_major_holiday: nmh,
            warnings,
            summary: cal.status_line(),
        };
        println!("{}", serde_json::to_string_pretty(&data)?);
        return Ok(());
    }

    print_card(&cal, target_date);
    Ok(())
}

fn print_card(cal: &CalendarDayInfo, date: NaiveDate) {
    let weekday = weekday_cn(date.weekday());
    println!("📅 公历日期: {} ({})", date.format("%Y-%m-%d"), weekday);
    println!("🌙 农历干支: {}", cal.lunar);
    println!("🏷  农历简述: {}", cal.lunar.short_format());

    if let Some(term) = cal.solar_term {
        println!("🌱 今日节气: {} · {}", term.name(), term.desc());
    } else {
        let (next_term, next_date, days) = cal.next_solar_term;
        println!(
            "🌱 下一节气: {} (距今 {} 天 · {}) · {}",
            next_term.name(),
            days,
            next_date.format("%Y-%m-%d"),
            next_term.desc()
        );
    }

    if !cal.holidays.is_empty() {
        println!("🎉 今日节日:");
        for h in &cal.holidays {
            if h.is_major {
                println!("   • [重大节日] {} — {}", h.name, h.hint);
            } else {
                println!("   • {} — {}", h.name, h.hint);
            }
        }
    }

    if !cal.warnings.is_empty() {
        println!("🏮 节日提醒 (重大节日规划):");
        for w in &cal.warnings {
            println!("   🔔  {}", w.message());
        }
    }

    if let Some((ref mh, m_date, days)) = cal.next_major_holiday {
        if days > 0 {
            println!(
                "📌 即将到来: 距{}还有 {} 天 ({}) · {}",
                mh.name,
                days,
                m_date.format("%Y-%m-%d"),
                mh.hint
            );
        }
    }

    println!("✨ 摘要一览: {}", cal.status_line());
}