Skip to main content

fur_cli/commands/
save.rs

1use clap::Parser;
2use serde_json::Value;
3use std::fs;
4use std::path::Path;
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
37        .join("threads")
38        .join(format!("{}.json", conversation_id));
39    let conversation: Value = serde_json::from_str(
40        &fs::read_to_string(&convo_path).expect("❌ Cannot read conversation"),
41    )
42    .unwrap();
43
44    let title = conversation["title"].as_str().unwrap_or("Untitled");
45    let safe_title = title.replace(" ", "_");
46
47    let output_path = args.out.unwrap_or_else(|| format!("{}.frs", safe_title));
48
49    let mut out = String::new();
50
51    // ---- header
52    out.push_str(&format!("new \"{}\"\n", title));
53    if let Some(tags) = conversation["tags"].as_array() {
54        if !tags.is_empty() {
55            let tags_str = tags
56                .iter()
57                .filter_map(|t| t.as_str())
58                .map(|t| format!("\"{}\"", t))
59                .collect::<Vec<_>>()
60                .join(", ");
61            out.push_str(&format!("tags = [{}]\n\n", tags_str));
62        }
63    }
64
65    // ---- messages (recursive)
66    for msg_id in conversation["messages"].as_array().unwrap_or(&vec![]) {
67        if let Some(mid) = msg_id.as_str() {
68            out.push_str(&render_message(mid, 0, fur_dir));
69        }
70    }
71
72    fs::write(&output_path, out).expect("❌ Could not write .frs file");
73    println!("💾 Saved conversation \"{}\" to {}", title, output_path);
74}
75
76fn render_message(msg_id: &str, indent: usize, fur_dir: &Path) -> String {
77    let msg_path = fur_dir.join("messages").join(format!("{}.json", msg_id));
78    let content = match fs::read_to_string(&msg_path) {
79        Ok(c) => c,
80        Err(_) => return String::new(),
81    };
82    let msg: Value = serde_json::from_str(&content).unwrap();
83
84    let mut out = String::new();
85    let pad = "    ".repeat(indent);
86
87    let avatar = msg["avatar"].as_str().unwrap_or("anon");
88
89    if let Some(text) = msg["text"].as_str() {
90        out.push_str(&format!("{}jot {} \"{}\"\n", pad, avatar, text));
91    } else if let Some(file) = msg["markdown"].as_str() {
92        out.push_str(&format!("{}jot {} --file \"{}\"\n", pad, avatar, file));
93    } else if let Some(att) = msg["attachment"].as_str() {
94        out.push_str(&format!("{}jot {} --img \"{}\"\n", pad, avatar, att));
95    }
96
97    if let Some(branches) = msg["branches"].as_array() {
98        for block in branches {
99            if let Some(arr) = block.as_array() {
100                out.push_str(&format!("{}branch {{\n", pad));
101                for child in arr {
102                    if let Some(cid) = child.as_str() {
103                        out.push_str(&render_message(cid, indent + 1, fur_dir));
104                    }
105                }
106                out.push_str(&format!("{}}}\n", pad));
107            }
108        }
109    }
110
111    out
112}