fur_cli/commands/
thread.rs

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