Skip to main content

fur_cli/commands/
timeline.rs

1use clap::Parser;
2use serde_json::{json, Value};
3use std::fs;
4use std::path::Path;
5
6use crate::renderer::{
7    markdown::render_message_to_md, pdf::export_convo_to_pdf, terminal::render_message,
8};
9
10/// Args for timeline command
11#[derive(Parser, Clone)]
12pub struct TimelineArgs {
13    #[arg(short, long)]
14    pub verbose: bool,
15    #[arg(long)]
16    pub contents: bool,
17    #[arg(long)]
18    pub out: Option<String>,
19
20    #[clap(skip)]
21    pub conversation_override: Option<String>,
22}
23
24/// Main entry for timeline
25pub fn run_timeline(args: TimelineArgs) {
26    let fur_dir = Path::new(".fur");
27    let index_path = fur_dir.join("index.json");
28    if !index_path.exists() {
29        eprintln!("🚨 .fur/ not found. Run `fur new` first.");
30        return;
31    }
32
33    // Load conversation metadata
34    let index: Value = serde_json::from_str(&fs::read_to_string(&index_path).unwrap()).unwrap();
35    let conversation_id = if let Some(ref override_id) = args.conversation_override {
36        override_id
37    } else {
38        index["active_thread"].as_str().unwrap_or("")
39    };
40
41    let convo_path = fur_dir
42        .join("threads")
43        .join(format!("{}.json", conversation_id));
44    let conversation_json: Value =
45        serde_json::from_str(&fs::read_to_string(&convo_path).unwrap()).unwrap();
46
47    let conversation_title = conversation_json["title"].as_str().unwrap_or("Untitled");
48
49    // Load avatars
50    let avatars: Value = serde_json::from_str(
51        &fs::read_to_string(fur_dir.join("avatars.json")).unwrap_or_else(|_| "{}".to_string()),
52    )
53    .unwrap_or(json!({}));
54
55    // Root messages (ids only)
56    let empty_vec: Vec<Value> = Vec::new();
57    let root_msgs = conversation_json["messages"]
58        .as_array()
59        .unwrap_or(&empty_vec);
60
61    // --- PDF mode
62    if let Some(path) = &args.out {
63        if path.ends_with(".pdf") {
64            export_convo_to_pdf(
65                &fur_dir,
66                conversation_title,
67                root_msgs,
68                &args,
69                &avatars,
70                path,
71            );
72            return;
73        }
74
75        // --- Markdown mode
76        let mut out_content = String::new();
77        out_content.push_str(&format!("# {}\n\n", conversation_title));
78
79        for mid in root_msgs {
80            if let Some(mid_str) = mid.as_str() {
81                render_message_to_md(
82                    &fur_dir,
83                    mid_str,
84                    "Root".to_string(),
85                    &args,
86                    &avatars,
87                    &mut out_content,
88                );
89            }
90        }
91
92        let word_count = out_content.split_whitespace().count();
93        let token_est = word_count * 4 / 3;
94
95        let mut final_output = String::new();
96        final_output.push_str(&format!(
97            "> ✍️ Words: {}\n> 🪙 Tokens (est.): {}\n\n",
98            word_count, token_est
99        ));
100        final_output.push_str(&out_content);
101
102        fs::write(path, final_output).expect("❌ Failed writing Markdown file");
103
104        println!("✔️ Timeline exported to {}", path);
105        return;
106    }
107
108    // --- Terminal mode
109    println!("Thread: {}", conversation_title);
110    for mid in root_msgs {
111        if let Some(mid_str) = mid.as_str() {
112            render_message(&fur_dir, mid_str, "Root".to_string(), &args, &avatars);
113        }
114    }
115}