@@ -3,6 +3,7 @@ use serde_json::{Value, json};
33use std:: fs;
44use std:: io:: { Write } ;
55use std:: path:: Path ;
6+ use crate :: helpers:: insertion:: run_insert;
67
78/// Subcommand: `fur msg`
89#[ derive( Parser , Debug ) ]
@@ -11,9 +12,14 @@ pub struct MsgArgs {
1112 #[ arg( index = 1 ) ]
1213 pub id_prefix : Option < String > ,
1314
14- /// Second positional: text
15- #[ arg( index = 2 ) ]
16- pub text_value : Option < String > ,
15+
16+ /// Insert before target
17+ #[ arg( long) ]
18+ pub pre : bool ,
19+
20+ /// Insert after target
21+ #[ arg( long) ]
22+ pub post : bool ,
1723
1824 #[ arg( long) ]
1925 pub edit : bool ,
@@ -29,16 +35,41 @@ pub struct MsgArgs {
2935
3036 #[ arg( long) ]
3137 pub interactive : bool ,
38+
39+ /// Everything *after* the ID
40+ #[ arg( index = 2 , trailing_var_arg = true ) ]
41+ pub rest : Vec < String > ,
42+
3243}
3344
45+
3446/// Entry point
3547pub fn run_msg ( args : MsgArgs ) {
48+
3649 if args. delete {
3750 return run_delete ( args) ;
3851 }
39- run_edit ( args) ;
52+
53+ // INSERT BEFORE
54+ if args. pre {
55+ return run_insert ( & args, true ) ;
56+ }
57+
58+ // INSERT AFTER
59+ if args. post {
60+ return run_insert ( & args, false ) ;
61+ }
62+
63+ if args. edit {
64+ return run_edit ( args) ;
65+ }
66+
67+ eprintln ! ( "❌ msg requires: --pre | --post | --edit | --delete" ) ;
4068}
4169
70+
71+
72+
4273//
4374// ======================================================
4475// DELETE LOGIC
@@ -55,7 +86,8 @@ fn run_delete(args: MsgArgs) {
5586
5687 let mut buf = String :: new ( ) ;
5788 std:: io:: stdin ( ) . read_line ( & mut buf) . unwrap ( ) ;
58- if ![ "y" , "Y" , "yes" , "YES" ] . contains ( & buf. trim ( ) ) {
89+
90+ if ![ "y" , "Y" , "yes" , "YES" ] . contains ( & buf. trim ( ) ) {
5991 println ! ( "❌ Cancelled." ) ;
6092 return ;
6193 }
@@ -74,35 +106,32 @@ fn run_delete(args: MsgArgs) {
74106//
75107
76108fn run_edit ( args : MsgArgs ) {
77- let ( id_opt, mut new_text) =
78- classify_id_and_text ( args. id_prefix , args. text_value ) ;
109+ let ( id_opt, mut text_opt) = classify_id_or_text ( & args) ;
79110
80111 // Final target message ID
81- let mid = id_opt. unwrap_or_else ( || resolve_target_message ( None ) ) ;
112+ let id = id_opt. unwrap_or_else ( || resolve_target_message ( None ) ) ;
82113
83114 let fur = Path :: new ( ".fur" ) ;
84- let msg_path = fur. join ( "messages" ) . join ( format ! ( "{}.json" , mid ) ) ;
115+ let msg_path = fur. join ( "messages" ) . join ( format ! ( "{}.json" , id ) ) ;
85116
86117 let mut msg: Value =
87118 serde_json:: from_str ( & fs:: read_to_string ( & msg_path) . unwrap ( ) ) . unwrap ( ) ;
88119
89120 // Interactive override
90121 if args. interactive {
91- let edited = run_interactive_editor (
92- msg[ "text" ] . as_str ( ) . unwrap_or_default ( )
93- ) ;
94- new_text = Some ( edited) ;
122+ let edited = run_interactive_editor ( msg[ "text" ] . as_str ( ) . unwrap_or_default ( ) ) ;
123+ text_opt = Some ( edited) ;
95124 }
96125
97126 // Apply text
98- if let Some ( t) = new_text {
127+ if let Some ( t) = text_opt {
99128 msg[ "text" ] = json ! ( t) ;
100129 msg[ "markdown" ] = json ! ( null) ;
101130 }
102131
103132 // Apply markdown
104- if let Some ( fpath ) = args. file {
105- msg[ "markdown" ] = json ! ( fpath ) ;
133+ if let Some ( fp ) = args. file {
134+ msg[ "markdown" ] = json ! ( fp ) ;
106135 msg[ "text" ] = json ! ( null) ;
107136 }
108137
@@ -113,66 +142,64 @@ fn run_edit(args: MsgArgs) {
113142
114143 write_json ( & msg_path, & msg) ;
115144
116- println ! ( "✏️ Edited {}" , & mid [ ..8 ] ) ;
145+ println ! ( "✏️ Edited {}" , & id [ ..8 ] ) ;
117146}
118147
148+
149+
119150//
120151// ======================================================
121- // POSITONAL ARG PARSING LOGIC
152+ // POSITONAL ID RESOLUTION
122153// ======================================================
123154//
124155
125- /// Detect if a value looks like a message ID prefix.
126- /// Returns Some(full_id) or None.
127- fn detect_id ( x : & Option < String > ) -> Option < String > {
128- let Some ( val) = x else { return None ; } ;
156+ /// Detect if value looks like an ID prefix.
157+ pub fn detect_id ( x : & Option < String > ) -> Option < String > {
158+ let Some ( val) = x else { return None } ;
129159
130- // positional that begins with "--" cannot be ID
131160 if val. starts_with ( "--" ) {
132161 return None ;
133162 }
134163
135- // Try to match existing prefix
136- if let Some ( id) = resolve_prefix_if_exists ( val) {
137- return Some ( id) ;
138- }
139-
140- None
164+ resolve_prefix_if_exists ( val)
141165}
142166
143- /// Interpret positionals into (id, text)
144- ///
145- /// Rules:
146- /// - If first positional matches a prefix → ID
147- /// - Second positional always text
148- /// - If first positional does NOT match → treat as text
149- fn classify_id_and_text (
150- id_prefix : Option < String > ,
151- text_value : Option < String >
152- ) -> ( Option < String > , Option < String > ) {
153-
154- // Case 1: first positional is a valid ID prefix
155- if id_prefix. is_some ( ) {
156- if let Some ( real_id) = detect_id ( & id_prefix) {
157- return ( Some ( real_id) , text_value) ;
167+
168+ /// Determine if the call looked like:
169+ /// msg <id> --edit new text...
170+ /// OR:
171+ /// msg "some text" --edit
172+ pub fn classify_id_or_text ( args : & MsgArgs ) -> ( Option < String > , Option < String > ) {
173+ // Case A: First positional *could* be an ID
174+ if let Some ( pfx) = & args. id_prefix {
175+ if let Some ( full_id) = detect_id ( & Some ( pfx. clone ( ) ) ) {
176+ // ID detected
177+ return ( Some ( full_id) , extract_text_from_rest ( args) ) ;
158178 }
159- }
160179
161- // Case 2: first positional is actually text
162- if let Some ( val) = id_prefix {
163- return ( None , Some ( val) ) ;
180+ // Not an ID → treat as text
181+ return ( None , Some ( pfx. clone ( ) ) ) ;
164182 }
165183
166- // Case 3: only second positional is provided
167- if let Some ( val) = text_value {
168- return ( None , Some ( val) ) ;
169- }
184+ // No id_prefix → rely on rest as text
185+ ( None , extract_text_from_rest ( args) )
186+ }
170187
171- ( None , None )
188+ /// Combine trailing args into text
189+ fn extract_text_from_rest ( args : & MsgArgs ) -> Option < String > {
190+ if args. rest . is_empty ( ) {
191+ None
192+ } else {
193+ Some ( args. rest . join ( " " ) )
194+ }
172195}
173196
197+ //
198+ // ======================================================
199+ // PREFIX UTILITIES
200+ // ======================================================
201+ //
174202
175- /// Internal helper: check for prefix match safely
176203fn resolve_prefix_if_exists ( pfx : & str ) -> Option < String > {
177204 let fur = Path :: new ( ".fur" ) ;
178205 let ( _index, tid) = resolve_active_conversation ( ) ;
@@ -181,15 +208,15 @@ fn resolve_prefix_if_exists(pfx: &str) -> Option<String> {
181208 let convo: Value =
182209 serde_json:: from_str ( & fs:: read_to_string ( & convo_path) . unwrap ( ) ) . unwrap ( ) ;
183210
184- let root = convo[ "messages" ]
211+ let root_ids = convo[ "messages" ]
185212 . as_array ( )
186213 . unwrap_or ( & vec ! [ ] )
187214 . iter ( )
188215 . filter_map ( |x| x. as_str ( ) . map ( |s| s. to_string ( ) ) )
189- . collect :: < Vec < String > > ( ) ;
216+ . collect :: < Vec < _ > > ( ) ;
190217
191218 let matches: Vec < & String > =
192- root . iter ( ) . filter ( |id| id. starts_with ( pfx) ) . collect ( ) ;
219+ root_ids . iter ( ) . filter ( |id| id. starts_with ( pfx) ) . collect ( ) ;
193220
194221 if matches. len ( ) == 1 {
195222 Some ( matches[ 0 ] . clone ( ) )
@@ -200,46 +227,47 @@ fn resolve_prefix_if_exists(pfx: &str) -> Option<String> {
200227
201228//
202229// ======================================================
203- // ID RESOLUTION HELPERS
230+ // ACTIVE CONVERSATION RESOLUTION
204231// ======================================================
205232//
206233
207234fn resolve_active_conversation ( ) -> ( Value , String ) {
208235 let idx_path = Path :: new ( ".fur/index.json" ) ;
209236 let index: Value =
210237 serde_json:: from_str ( & fs:: read_to_string ( idx_path) . unwrap ( ) ) . unwrap ( ) ;
238+
211239 let tid = index[ "active_thread" ] . as_str ( ) . unwrap_or ( "" ) . to_string ( ) ;
240+
212241 ( index, tid)
213242}
214243
215- fn resolve_target_message ( prefix : Option < String > ) -> String {
244+ pub fn resolve_target_message ( prefix : Option < String > ) -> String {
216245 let fur = Path :: new ( ".fur" ) ;
217246
218247 let ( index, tid) = resolve_active_conversation ( ) ;
219248 let convo_path = fur. join ( "threads" ) . join ( format ! ( "{}.json" , tid) ) ;
249+
220250 let convo: Value =
221251 serde_json:: from_str ( & fs:: read_to_string ( & convo_path) . unwrap ( ) ) . unwrap ( ) ;
222252
223- let root = convo[ "messages" ]
253+ let root_ids = convo[ "messages" ]
224254 . as_array ( )
225- . unwrap_or ( & vec ! [ ] )
255+ . unwrap ( )
226256 . iter ( )
227257 . filter_map ( |v| v. as_str ( ) . map ( |s| s. to_string ( ) ) )
228- . collect :: < Vec < String > > ( ) ;
258+ . collect :: < Vec < _ > > ( ) ;
229259
230- if let Some ( p ) = prefix {
231- return resolve_prefix ( & root , & p ) ;
260+ if let Some ( pfx ) = prefix {
261+ return resolve_prefix ( & root_ids , & pfx ) ;
232262 }
233263
234- // current_message wins
235264 if let Some ( cur) = index[ "current_message" ] . as_str ( ) {
236265 if !cur. is_empty ( ) {
237266 return cur. to_string ( ) ;
238267 }
239268 }
240269
241- // fallback → last root
242- root. last ( ) . expect ( "❌ No messages" ) . to_string ( )
270+ root_ids. last ( ) . expect ( "❌ No messages" ) . to_string ( )
243271}
244272
245273fn resolve_prefix ( root_ids : & Vec < String > , prefix : & str ) -> String {
@@ -268,19 +296,12 @@ fn recursive_delete(mid: &str) {
268296 let fur = Path :: new ( ".fur" ) ;
269297 let msg_path = fur. join ( "messages" ) . join ( format ! ( "{}.json" , mid) ) ;
270298
271- let content = match fs:: read_to_string ( & msg_path) {
272- Ok ( c) => c,
273- Err ( _) => return ,
274- } ;
275-
276- let msg: Value = match serde_json:: from_str ( & content) {
277- Ok ( v) => v,
278- Err ( _) => return ,
279- } ;
299+ let Ok ( content) = fs:: read_to_string ( & msg_path) else { return } ;
300+ let Ok ( msg) = serde_json:: from_str :: < Value > ( & content) else { return } ;
280301
281302 if let Some ( children) = msg[ "children" ] . as_array ( ) {
282- for c in children {
283- if let Some ( cid) = c . as_str ( ) {
303+ for child in children {
304+ if let Some ( cid) = child . as_str ( ) {
284305 recursive_delete ( cid) ;
285306 }
286307 }
@@ -292,11 +313,12 @@ fn recursive_delete(mid: &str) {
292313fn remove_from_parent_or_root ( mid : & str ) {
293314 let fur = Path :: new ( ".fur" ) ;
294315
316+ // Load deleted msg metadata (if exists)
295317 let msg_path = fur. join ( "messages" ) . join ( format ! ( "{}.json" , mid) ) ;
296318 let raw = fs:: read_to_string ( & msg_path) . unwrap_or ( "{}" . into ( ) ) ;
297319 let msg: Value = serde_json:: from_str ( & raw ) . unwrap_or ( json ! ( { } ) ) ;
298320
299- // If message had a parent
321+ // If part of a thread tree
300322 if let Some ( pid) = msg[ "parent" ] . as_str ( ) {
301323 let ppath = fur. join ( "messages" ) . join ( format ! ( "{}.json" , pid) ) ;
302324 if let Ok ( content) = fs:: read_to_string ( & ppath) {
@@ -309,9 +331,10 @@ fn remove_from_parent_or_root(mid: &str) {
309331 return ;
310332 }
311333
312- // Else: it's root-level in conversation
334+ // Otherwise part of root list
313335 let ( _index, tid) = resolve_active_conversation ( ) ;
314336 let convo_path = fur. join ( "threads" ) . join ( format ! ( "{}.json" , tid) ) ;
337+
315338 let mut convo: Value =
316339 serde_json:: from_str ( & fs:: read_to_string ( & convo_path) . unwrap ( ) ) . unwrap ( ) ;
317340
@@ -325,6 +348,7 @@ fn remove_from_parent_or_root(mid: &str) {
325348fn update_current_after_delete ( mid : & str ) {
326349 let fur = Path :: new ( ".fur" ) ;
327350 let idx_path = fur. join ( "index.json" ) ;
351+
328352 let mut index: Value =
329353 serde_json:: from_str ( & fs:: read_to_string ( & idx_path) . unwrap ( ) ) . unwrap ( ) ;
330354
0 commit comments