fur_cli/helpers/
conversation.rs1use std::fs;
2use std::path::{Path};
3use serde_json::{Value, json};
4use std::io::{self, Write};
5use crate::commands::conversation::ThreadArgs;
6use colored::*;
7
8pub fn resolve_target_thread_id(
9 index: &Value,
10 args: &ThreadArgs,
11) -> Option<String> {
12 let empty_vec: Vec<Value> = Vec::new();
13 let threads: Vec<String> = index["threads"]
14 .as_array()
15 .unwrap_or(&empty_vec)
16 .iter()
17 .filter_map(|t| t.as_str().map(|s| s.to_string()))
18 .collect();
19
20 if let Some(prefix) = &args.id {
22 let matches: Vec<&String> = threads
23 .iter()
24 .filter(|tid| tid.starts_with(prefix))
25 .collect();
26
27 return match matches.as_slice() {
28 [] => {
29 eprintln!("❌ No conversation matches '{}'", prefix);
30 None
31 }
32 [single] => Some((*single).clone()),
33 _ => {
34 eprintln!("❌ Ambiguous prefix '{}': {:?}", prefix, matches);
35 None
36 }
37 };
38 }
39
40 let active = index["active_thread"].as_str().unwrap_or("").to_string();
42 if active.is_empty() {
43 eprintln!("❌ No active conversation to delete.");
44 return None;
45 }
46
47 Some(active)
48}
49
50pub fn confirm_delete_primary() -> bool {
51
52 println!("Are you sure you want to delete this conversation? (y/N)");
53 print!("> ");
54 io::stdout().flush().unwrap();
55
56 let mut input = String::new();
57 io::stdin().read_line(&mut input).unwrap();
58
59 input.trim().to_lowercase() == "y"
60}
61
62pub fn confirm_delete_destructive() -> bool {
63
64 println!();
65 println!(
66 "{}",
67 "⚠️ Reminder: deleting a conversation is a destructive action.\n\
68 It cannot be reversed unless the project is version-controlled (git)."
69 .color(Color::BrightRed)
70 .bold()
71 );
72 println!();
73 println!("Type DELETE to confirm:");
74 print!("> ");
75 io::stdout().flush().unwrap();
76
77 let mut input = String::new();
78 io::stdin().read_line(&mut input).unwrap();
79
80 input.trim() == "DELETE"
81}
82
83pub fn perform_conversation_deletion(
84 index: &mut Value,
85 fur_dir: &Path,
86 target_tid: &str,
87 threads: &[String],
88) {
89 let convo_path = fur_dir.join("threads").join(format!("{}.json", target_tid));
90
91 let convo_content = fs::read_to_string(&convo_path)
93 .expect("Failed to load conversation JSON.");
94 let convo: Value = serde_json::from_str(&convo_content).unwrap();
95
96 let title = convo["title"].as_str().unwrap_or("Untitled");
97 let msg_ids: Vec<String> = convo["messages"]
98 .as_array()
99 .unwrap_or(&vec![])
100 .iter()
101 .filter_map(|v| v.as_str().map(|s| s.to_string()))
102 .collect();
103
104 println!(
105 "🗑️ Deleting conversation {} \"{}\"...",
106 &target_tid[..8],
107 title
108 );
109
110 let _ = fs::remove_file(&convo_path);
112
113 for mid in msg_ids {
115 let msg_path = fur_dir.join("messages").join(format!("{}.json", mid));
116
117 if let Ok(content) = fs::read_to_string(&msg_path) {
118 if let Ok(msg_json) = serde_json::from_str::<Value>(&content) {
119 if let Some(md_raw) = msg_json["markdown"].as_str() {
120 let md_path = Path::new(md_raw);
121 if md_path.is_absolute() {
122 let _ = fs::remove_file(md_path);
123 } else {
124 let _ = fs::remove_file(Path::new(".").join(md_raw));
125 }
126 }
127 }
128 }
129
130 let _ = fs::remove_file(&msg_path);
131 }
132
133 let new_threads: Vec<String> = threads
135 .iter()
136 .filter(|tid| tid.as_str() != target_tid)
137 .cloned()
138 .collect();
139
140 index["threads"] = json!(new_threads);
141
142 if index["active_thread"].as_str() == Some(target_tid) {
144 index["active_thread"] = Value::Null;
145 index["current_message"] = Value::Null;
146 }
147
148 let index_path = fur_dir.join("index.json");
150 fs::write(&index_path, serde_json::to_string_pretty(&index).unwrap()).unwrap();
151
152 println!("✔️ Conversation deleted successfully.");
153}