fur_cli/commands/
timeline.rs1use 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#[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
28pub 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 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 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 let empty_vec: Vec<Value> = Vec::new();
58 let root_msgs = conversation_json["messages"].as_array().unwrap_or(&empty_vec);
59
60 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 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 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}