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