fur_cli/commands/
timeline.rs

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