1use clap::Parser;
2use std::fs;
3use std::path::Path;
4use serde_json::Value;
5
6#[derive(Parser)]
8pub struct SaveArgs {
9 #[arg(short, long)]
11 pub out: Option<String>,
12}
13
14pub fn run_save(args: SaveArgs) {
16 let fur_dir = Path::new(".fur");
17 let index_path = fur_dir.join("index.json");
18
19 if !index_path.exists() {
20 eprintln!("🚨 .fur/ not found. Run `fur new` first.");
21 return;
22 }
23
24 let index: Value =
25 serde_json::from_str(&fs::read_to_string(&index_path).expect("❌ Cannot read index.json"))
26 .unwrap();
27
28 let thread_id = match index["active_thread"].as_str() {
29 Some(id) => id,
30 None => {
31 eprintln!("⚠️ No active thread.");
32 return;
33 }
34 };
35
36 let thread_path = fur_dir.join("threads").join(format!("{}.json", thread_id));
37 let thread: Value =
38 serde_json::from_str(&fs::read_to_string(&thread_path).expect("❌ Cannot read thread"))
39 .unwrap();
40
41 let title = thread["title"].as_str().unwrap_or("Untitled");
42 let safe_title = title.replace(" ", "_");
43
44 let output_path = args
45 .out
46 .unwrap_or_else(|| format!("{}.frs", safe_title));
47
48 let mut out = String::new();
49
50 out.push_str(&format!("new \"{}\"\n", title));
52 if let Some(tags) = thread["tags"].as_array() {
53 if !tags.is_empty() {
54 let tags_str = tags
55 .iter()
56 .filter_map(|t| t.as_str())
57 .map(|t| format!("\"{}\"", t))
58 .collect::<Vec<_>>()
59 .join(", ");
60 out.push_str(&format!("tags = [{}]\n\n", tags_str));
61 }
62 }
63
64 for msg_id in thread["messages"].as_array().unwrap_or(&vec![]) {
66 if let Some(mid) = msg_id.as_str() {
67 out.push_str(&render_message(mid, 0, fur_dir));
68 }
69 }
70
71 fs::write(&output_path, out).expect("❌ Could not write .frs file");
72 println!("💾 Saved thread \"{}\" to {}", title, output_path);
73}
74
75fn render_message(msg_id: &str, indent: usize, fur_dir: &Path) -> String {
76 let msg_path = fur_dir.join("messages").join(format!("{}.json", msg_id));
77 let content = match fs::read_to_string(&msg_path) {
78 Ok(c) => c,
79 Err(_) => return String::new(),
80 };
81 let msg: Value = serde_json::from_str(&content).unwrap();
82
83 let mut out = String::new();
84 let pad = " ".repeat(indent);
85
86 let avatar = msg["avatar"].as_str().unwrap_or("anon");
87
88 if let Some(text) = msg["text"].as_str() {
89 out.push_str(&format!("{}jot {} \"{}\"\n", pad, avatar, text));
90 } else if let Some(file) = msg["markdown"].as_str() {
91 out.push_str(&format!("{}jot {} --file \"{}\"\n", pad, avatar, file));
92 } else if let Some(att) = msg["attachment"].as_str() {
93 out.push_str(&format!("{}jot {} --img \"{}\"\n", pad, avatar, att));
94 }
95
96 if let Some(branches) = msg["branches"].as_array() {
97 for block in branches {
98 if let Some(arr) = block.as_array() {
99 out.push_str(&format!("{}branch {{\n", pad));
100 for child in arr {
101 if let Some(cid) = child.as_str() {
102 out.push_str(&render_message(cid, indent + 1, fur_dir));
103 }
104 }
105 out.push_str(&format!("{}}}\n", pad));
106 }
107 }
108 }
109
110 out
111}