fur_cli/commands/
thread.rs

1use std::fs;
2use std::path::Path;
3use serde_json::{Value, json};
4use clap::Parser;
5
6/// Arguments for the `thread` command
7#[derive(Parser)]
8pub struct ThreadArgs {
9    /// Thread ID or prefix to switch
10    pub id: Option<String>,
11
12    /// View all threads
13    #[arg(long)]
14    pub view: bool,
15}
16
17/// Main entry point for the `thread` command
18pub fn run_thread(args: ThreadArgs) {
19    let fur_dir = Path::new(".fur");
20    let index_path = fur_dir.join("index.json");
21
22    if !index_path.exists() {
23        eprintln!("🚨 .fur/ not found. Run `fur new` first.");
24        return;
25    }
26
27    let mut index: Value =
28        serde_json::from_str(&fs::read_to_string(&index_path).unwrap()).unwrap();
29
30    // ------------------------
31    // VIEW ALL THREADS
32    // ------------------------
33    if args.view || args.id.is_none() {
34        println!("📇 Threads in .fur:");
35
36        let empty_vec: Vec<Value> = Vec::new();
37        let threads = index["threads"].as_array().unwrap_or(&empty_vec);
38        let active = index["active_thread"].as_str().unwrap_or("");
39
40        for tid in threads {
41            if let Some(tid_str) = tid.as_str() {
42                let thread_path = fur_dir.join("threads").join(format!("{}.json", tid_str));
43                if let Ok(content) = fs::read_to_string(thread_path) {
44                    if let Ok(thread_json) = serde_json::from_str::<Value>(&content) {
45                        let title = thread_json["title"].as_str().unwrap_or("Untitled");
46                        let marker = if tid_str == active { "⭐ Active" } else { " " };
47                        println!("{marker} {tid_str:.8}  \"{title}\"");
48                    }
49                }
50            }
51        }
52        return;
53    }
54
55    // ------------------------
56    // SWITCH ACTIVE THREAD
57    // ------------------------
58    if let Some(tid) = args.id {
59        let empty_vec: Vec<Value> = Vec::new();
60        let threads: Vec<String> = index["threads"]
61            .as_array()
62            .unwrap_or(&empty_vec)
63            .iter()
64            .filter_map(|t| t.as_str().map(|s| s.to_string()))
65            .collect();
66
67        // Try exact match first
68        let mut found = threads.iter().find(|&s| s == &tid);
69
70        // If no exact match, try prefix match
71        if found.is_none() {
72            let matches: Vec<&String> = threads
73                .iter()
74                .filter(|s| s.starts_with(&tid))
75                .collect();
76
77            if matches.len() == 1 {
78                found = Some(matches[0]);
79            } else if matches.len() > 1 {
80                eprintln!("❌ Ambiguous prefix '{}'. Matches: {:?}", tid, matches);
81                return;
82            }
83        }
84
85        let tid_full = match found {
86            Some(s) => s,
87            None => {
88                eprintln!("❌ Thread not found: {}", tid);
89                return;
90            }
91        };
92
93        // ✅ Now we can safely mutate index
94        index["active_thread"] = json!(tid_full);
95        index["current_message"] = serde_json::Value::Null;
96        fs::write(&index_path, serde_json::to_string_pretty(&index).unwrap()).unwrap();
97
98        let thread_path = fur_dir.join("threads").join(format!("{}.json", tid_full));
99        let content = fs::read_to_string(thread_path).unwrap();
100        let thread_json: Value = serde_json::from_str(&content).unwrap();
101        let title = thread_json["title"].as_str().unwrap_or("Untitled");
102
103        println!("✔️ Switched active thread to {} \"{}\"", &tid_full[..8], title);
104    }
105}