fur_cli/helpers/
cloning.rs1use std::fs;
2use std::path::{Path, PathBuf};
3use serde_json::{Value, json};
4use uuid::Uuid;
5use chrono::Utc;
6use std::collections::HashMap;
7
8pub fn load_conversation_metadata(path: &Path) -> (String, Vec<String>) {
9 let convo: Value =
10 serde_json::from_str(&fs::read_to_string(path).unwrap()).unwrap();
11
12 let title = convo["title"].as_str().unwrap_or("Untitled").to_string();
13
14 let messages = convo["messages"]
15 .as_array()
16 .unwrap_or(&vec![])
17 .iter()
18 .filter_map(|v| v.as_str().map(|s| s.to_string()))
19 .collect::<Vec<_>>();
20
21 (title, messages)
22}
23
24pub fn make_new_conversation_header(
25 old_title: &str,
26 custom_title: Option<String>,
27) -> (String, String, String) {
28 let new_id = Uuid::new_v4().to_string();
29 let timestamp = Utc::now().to_rfc3339();
30 let new_title = custom_title.unwrap_or_else(|| format!("Clone of {}", old_title));
31 (new_id, new_title, timestamp)
32}
33
34pub fn build_id_remap(old_messages: &[String]) -> HashMap<String, String> {
35 let mut map = HashMap::new();
36 for old in old_messages {
37 map.insert(old.clone(), Uuid::new_v4().to_string());
38 }
39 map
40}
41
42pub fn clone_all_messages(id_map: &HashMap<String, String>, old_messages: &[String]) {
43 let messages_dir = Path::new(".fur/messages");
44
45 for old_id in old_messages {
46 let old_msg_path = messages_dir.join(format!("{}.json", old_id));
47
48 let old_msg: Value =
49 serde_json::from_str(&fs::read_to_string(&old_msg_path).unwrap()).unwrap();
50
51 let new_id = id_map.get(old_id).unwrap();
52
53 let new_parent = remap_optional(&old_msg["parent"], id_map);
54 let new_children = remap_vec(&old_msg["children"], id_map);
55 let new_branches = remap_vec(&old_msg["branches"], id_map);
56
57 let new_markdown = clone_markdown_if_any(&old_msg);
59
60 let mut new_msg = old_msg.clone();
61 new_msg["id"] = json!(new_id);
62 new_msg["timestamp"] = json!(Utc::now().to_rfc3339());
63 new_msg["parent"] = new_parent;
64 new_msg["children"] = json!(new_children);
65 new_msg["branches"] = json!(new_branches);
66 new_msg["markdown"] = match new_markdown {
67 Some(path) => json!(path),
68 None => Value::Null,
69 };
70
71 let new_path = messages_dir.join(format!("{}.json", new_id));
73 fs::write(new_path, serde_json::to_string_pretty(&new_msg).unwrap()).unwrap();
74 }
75}
76
77pub fn remap_optional(val: &Value, map: &HashMap<String, String>) -> Value {
78 match val.as_str().and_then(|v| map.get(v)) {
79 Some(new) => json!(new),
80 None => Value::Null,
81 }
82}
83
84pub fn remap_vec(val: &Value, map: &HashMap<String, String>) -> Vec<Value> {
85 val.as_array()
86 .unwrap_or(&vec![])
87 .iter()
88 .filter_map(|v| v.as_str())
89 .filter_map(|old| map.get(old))
90 .map(|new| json!(new))
91 .collect()
92}
93
94pub fn clone_markdown_if_any(old_msg: &Value) -> Option<String> {
95 if let Some(md_raw) = old_msg["markdown"].as_str() {
96 let old_md_path = PathBuf::from(md_raw);
97
98 if old_md_path.exists() {
99 let filename = old_md_path.file_name()?.to_string_lossy();
101
102 let stem = filename.trim_end_matches(".md");
104 let (base, existing_c_suffix) = split_clone_suffix(stem);
105
106 let new_suffix = format!("{}c", existing_c_suffix);
108
109 let new_filename = format!("{}{}.md", base, new_suffix);
110 let new_path = format!("chats/{}", new_filename);
111
112 fs::copy(&old_md_path, &new_path)
113 .expect("❌ Failed to copy markdown file");
114
115 return Some(new_path);
116 }
117 }
118 None
119}
120
121fn split_clone_suffix(stem: &str) -> (String, String) {
123 let c_count = stem.chars().rev().take_while(|&ch| ch == 'c').count();
125
126 if c_count == 0 {
127 return (stem.to_string(), String::new());
129 }
130
131 let base_len = stem.len() - c_count;
132 let base = stem[..base_len].to_string();
133 let suffix = stem[base_len..].to_string();
134
135 (base, suffix)
136}
137
138
139pub fn write_new_conversation(
140 new_id: &str,
141 new_title: &str,
142 timestamp: &str,
143 id_map: &HashMap<String, String>,
144 old_messages: &[String],
145) {
146 let threads_dir = Path::new(".fur/threads");
147
148 let new_messages: Vec<String> = old_messages
149 .iter()
150 .map(|old| id_map.get(old).unwrap().clone())
151 .collect();
152
153 let convo = json!({
154 "id": new_id,
155 "title": new_title,
156 "created_at": timestamp,
157 "messages": new_messages,
158 "tags": []
159 });
160
161 let new_path = threads_dir.join(format!("{}.json", new_id));
162 fs::write(new_path, serde_json::to_string_pretty(&convo).unwrap()).unwrap();
163}
164
165pub fn update_index(new_id: &str) {
166 let index_path = Path::new(".fur/index.json");
167 let mut index: Value =
168 serde_json::from_str(&fs::read_to_string(&index_path).unwrap()).unwrap();
169
170 index["threads"]
171 .as_array_mut()
172 .unwrap()
173 .push(json!(new_id));
174
175 index["active_thread"] = json!(new_id);
176 index["current_message"] = Value::Null;
177
178 fs::write(index_path, serde_json::to_string_pretty(&index).unwrap()).unwrap();
179}