Skip to main content

fur_cli/commands/
save.rs

1use clap::Parser;
2use std::fs;
3use std::path::Path;
4use serde_json::Value;
5
6/// Arguments for the `save` subcommand
7#[derive(Parser)]
8pub struct SaveArgs {
9    /// Output path for the .frs file
10    #[arg(short, long)]
11    pub out: Option<String>,
12}
13
14/// Save the active conversation back into a .frs file
15pub 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 conversation_id = match index["active_thread"].as_str() {
29        Some(id) => id,
30        None => {
31            eprintln!("⚠️ No active conversation.");
32            return;
33        }
34    };
35
36    let convo_path = fur_dir.join("threads").join(format!("{}.json", conversation_id));
37    let conversation: Value =
38        serde_json::from_str(&fs::read_to_string(&convo_path).expect("❌ Cannot read conversation"))
39            .unwrap();
40
41    let title = conversation["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    // ---- header
51    out.push_str(&format!("new \"{}\"\n", title));
52    if let Some(tags) = conversation["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    // ---- messages (recursive)
65    for msg_id in conversation["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 conversation \"{}\" 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}