Skip to main content

fur_cli/commands/
jump.rs

1use clap::Args;
2use serde_json::Value;
3use std::fs;
4use std::path::Path;
5
6/// JumpArgs allow specifying relative or absolute jumps
7#[derive(Args, Debug)]
8pub struct JumpArgs {
9    #[arg(short, long)]
10    pub past: Option<usize>,
11
12    #[arg(short, long)]
13    pub child: Option<usize>,
14
15    #[arg(short, long)]
16    pub id: Option<String>,
17}
18
19pub fn run_jump(args: JumpArgs) -> Result<(), Box<dyn std::error::Error>> {
20    let index_path = Path::new(".fur/index.json");
21    let index_data = fs::read_to_string(index_path).expect("❌ Couldn't read index.json");
22    let mut index: Value = serde_json::from_str(&index_data).unwrap();
23
24    let conversation_id = index["active_thread"].as_str().unwrap();
25    let convo_path = Path::new(".fur/threads").join(format!("{}.json", conversation_id));
26    let conversation_data =
27        fs::read_to_string(convo_path).expect("❌ Couldn't read conversation file");
28    let conversation: Value = serde_json::from_str(&conversation_data).unwrap();
29
30    let current_id = index["current_message"].as_str().unwrap_or_default();
31
32    let messages = conversation["messages"].as_array().unwrap();
33
34    // Locate current message
35    let current_msg_id = current_id;
36    let current_msg = messages.iter().find_map(|id| {
37        if id.as_str()? == current_msg_id {
38            let msg_path = Path::new(".fur/messages").join(format!("{}.json", current_msg_id));
39            let msg_data = fs::read_to_string(msg_path).ok()?;
40            serde_json::from_str::<Value>(&msg_data).ok()
41        } else {
42            None
43        }
44    });
45
46    if current_msg.is_none() {
47        eprintln!("❌ Current message not found in conversation.");
48        return Ok(());
49    }
50    let current = current_msg.unwrap();
51
52    // Handle jump --past
53    if let Some(n) = args.past {
54        let mut current_id = current["id"].as_str().unwrap_or_default().to_string();
55        let mut jumped = 0;
56
57        while jumped < n {
58            // println!("🔍 Loop iteration {} — current_id: {}", jumped, current_id);
59
60            let msg_path = Path::new(".fur/messages").join(format!("{}.json", current_id));
61            let msg_data = fs::read_to_string(&msg_path);
62            if msg_data.is_err() {
63                eprintln!("❌ Failed to load message: {}", current_id);
64                return Ok(());
65            }
66
67            let msg_json: Value = serde_json::from_str(&msg_data.unwrap()).unwrap();
68
69            // let parent_raw = &msg_json["parent"];
70            // println!("   ↪️ parent field raw: {}", parent_raw);
71
72            match msg_json["parent"].as_str() {
73                Some(pid) if !pid.is_empty() => {
74                    let in_conversation = conversation["messages"]
75                        .as_array()
76                        .unwrap_or(&vec![])
77                        .iter()
78                        .any(|val| val.as_str() == Some(pid));
79
80                    if !in_conversation {
81                        println!("\x1b[91m📜 You've reached the origin of this conversation. No earlier messages exist.\x1b[0m");
82                        println!("\x1b[93m🌱 To start a new conversation, run:\n    fur new \"Title of your new conversation\"\x1b[0m");
83                        return Ok(());
84                    }
85
86                    // println!("   🧬 Jumping to parent_id: {}", pid);
87                    current_id = pid.to_string();
88                    jumped += 1;
89                }
90                _ => {
91                    println!("\x1b[91m📜 You've reached the origin of this conversation. No earlier messages exist.\x1b[0m");
92                    println!("\x1b[93m🌱 To start a new conversation, run:\n    fur new \"Title of your new conversation\"\x1b[0m");
93                    return Ok(());
94                }
95            }
96        }
97
98        index["current_message"] = Value::String(current_id.clone());
99        fs::write(index_path, serde_json::to_string_pretty(&index).unwrap()).unwrap();
100        println!(
101            "⏪ Jumped back {} message{} to {}",
102            jumped,
103            if jumped == 1 { "" } else { "s" },
104            current_id
105        );
106        return Ok(());
107    }
108
109    // Handle jump --child
110    if let Some(n) = args.child {
111        let current_id = current["id"].as_str().unwrap_or_default();
112
113        let children: Vec<String> = messages
114            .iter()
115            .filter_map(|msg_id| {
116                let msg_path =
117                    Path::new(".fur/messages").join(format!("{}.json", msg_id.as_str()?));
118                let msg_data = fs::read_to_string(msg_path).ok()?;
119                let msg_json: Value = serde_json::from_str(&msg_data).ok()?;
120                if msg_json["parent"].as_str()? == current_id {
121                    Some(msg_json["id"].as_str()?.to_string())
122                } else {
123                    None
124                }
125            })
126            .collect();
127
128        if let Some(child_id) = children.get(n) {
129            index["current_message"] = Value::String(child_id.to_string());
130            fs::write(index_path, serde_json::to_string_pretty(&index).unwrap()).unwrap();
131            println!("⏩ Jumped to child [{}]: {}", n, child_id);
132            return Ok(());
133        } else {
134            eprintln!("❌ No such child at index {}", n);
135            return Ok(());
136        }
137    }
138
139    // Handle jump --id
140    if let Some(ref target_id) = args.id {
141        if messages
142            .iter()
143            .any(|m| m["id"].as_str() == Some(&target_id))
144        {
145            index["current_message"] = Value::String(target_id.to_string());
146            fs::write(index_path, serde_json::to_string_pretty(&index).unwrap()).unwrap();
147            println!("🎯 Jumped directly to message ID {}", target_id);
148            return Ok(());
149        } else {
150            eprintln!("❌ Message ID not found: {}", target_id);
151            return Ok(());
152        }
153    }
154
155    eprintln!("❗ No jump argument provided. Use --past, --child, or --id.");
156    Ok(())
157}