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_md,
9    pdf::export_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 thread_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 thread metadata
38    let index: Value = serde_json::from_str(&fs::read_to_string(&index_path).unwrap()).unwrap();
39    let thread_id = if let Some(ref override_id) = args.thread_override {
40        override_id
41    } else {
42        index["active_thread"].as_str().unwrap_or("")
43    };
44
45    let thread_path = fur_dir.join("threads").join(format!("{}.json", thread_id));
46    let thread_json: Value = serde_json::from_str(&fs::read_to_string(&thread_path).unwrap()).unwrap();
47
48    let thread_title = thread_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 = thread_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_to_pdf(&fur_dir, thread_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", thread_title));
71
72        for mid in root_msgs {
73            if let Some(mid_str) = mid.as_str() {
74                render_message_md(&fur_dir, mid_str, "Root".to_string(), &args, &avatars, &mut out_content);
75            }
76        }
77
78        fs::write(path, out_content).expect("❌ Failed writing Markdown file");
79        println!("✔️ Timeline exported to {}", path);
80        return;
81    }
82
83    // --- Terminal mode
84    println!("Thread: {}", thread_title);
85    for mid in root_msgs {
86        if let Some(mid_str) = mid.as_str() {
87            render_message(&fur_dir, mid_str, "Root".to_string(), &args, &avatars);
88        }
89    }
90}