Skip to main content

fur_cli/commands/
run.rs

1use crate::commands::timeline::TimelineArgs;
2use crate::commands::tree::TreeArgs;
3use crate::commands::{timeline, tree};
4use crate::frs::{parser, persist_frs};
5use colored::*;
6use std::fs;
7
8/// Run an .frs script:
9/// - Parse into Thread (in-memory)
10/// - Execute inline commands (tree, timeline, status)
11/// - Persist once at first `store`
12/// - Ignore later `store`s
13pub fn run_frs(path: &str) {
14    let raw = fs::read_to_string(path)
15        .unwrap_or_else(|_| panic!("❌ Could not read .frs file: {}", path));
16
17    let lines: Vec<String> = raw
18        .lines()
19        .map(|l| l.trim().to_string())
20        .filter(|l| !l.is_empty() && !l.starts_with('#'))
21        .collect();
22
23    let conversation = parser::parse_frs(path);
24    let mut stored = false;
25
26    for (lineno, line) in lines.iter().enumerate() {
27        // --- Commit point
28        if line == "store" {
29            if !stored {
30                let tid = persist_frs(&conversation);
31                println!("✔️ Thread persisted at line {} → {}", lineno + 1, &tid[..8]);
32                stored = true;
33            } else {
34                eprintln!(
35                    "{}",
36                    format!(
37                        "⚠️ Ignoring extra `store` at line {} — already persisted",
38                        lineno + 1
39                    )
40                    .yellow()
41                    .bold()
42                );
43            }
44            continue;
45        }
46
47        // --- Status
48        if line.starts_with("status") {
49            with_ephemeral(stored, &conversation, |tid_override| {
50                let args = crate::commands::status::StatusArgs {
51                    conversation_override: tid_override,
52                };
53                crate::commands::status::run_status(args);
54            });
55            continue;
56        }
57
58        // --- Timeline
59        if line.starts_with("timeline") {
60            let parts: Vec<&str> = line.split_whitespace().collect();
61            let mut args = TimelineArgs {
62                verbose: false,
63                contents: false,
64                out: None,
65                conversation_override: None,
66            };
67            for (i, p) in parts.iter().enumerate() {
68                if *p == "--out" {
69                    args.out = parts.get(i + 1).map(|s| s.to_string());
70                }
71                if *p == "--contents" {
72                    args.contents = true;
73                }
74            }
75
76            with_ephemeral(stored, &conversation, |tid_override| {
77                let mut args = args.clone();
78                args.conversation_override = tid_override;
79                timeline::run_timeline(args);
80            });
81            continue;
82        }
83
84        // --- Printed
85        if line.starts_with("printed") {
86            let parts: Vec<&str> = line.split_whitespace().collect();
87            let mut out: Option<String> = None;
88            let mut verbose = false;
89
90            for (i, p) in parts.iter().enumerate() {
91                if *p == "--out" {
92                    out = parts.get(i + 1).map(|s| s.to_string());
93                }
94                if *p == "--verbose" || *p == "-v" {
95                    verbose = true;
96                }
97            }
98
99            with_ephemeral(stored, &conversation, |tid_override| {
100                // Load index.json so we can temporarily override active thread
101                let index_path = std::path::Path::new(".fur").join("index.json");
102                let mut index_json: serde_json::Value =
103                    serde_json::from_str(&std::fs::read_to_string(&index_path).unwrap()).unwrap();
104
105                let original_active = index_json["active_thread"].as_str().map(|s| s.to_string());
106
107                if let Some(tid) = &tid_override {
108                    index_json["active_thread"] = tid.clone().into();
109                    std::fs::write(
110                        &index_path,
111                        serde_json::to_string_pretty(&index_json).unwrap(),
112                    )
113                    .unwrap();
114                }
115
116                // Now run printed, which will read this modified active_thread
117                crate::commands::printed::run_printed(out.clone(), verbose);
118
119                // Restore original active_thread
120                if let Some(orig) = original_active {
121                    index_json["active_thread"] = orig.into();
122                    std::fs::write(
123                        &index_path,
124                        serde_json::to_string_pretty(&index_json).unwrap(),
125                    )
126                    .unwrap();
127                }
128            });
129
130            continue;
131        }
132
133        // --- Tree
134        if line.starts_with("tree") {
135            let args = TreeArgs {
136                conversation_override: None,
137            };
138            with_ephemeral(stored, &conversation, |tid_override| {
139                let mut args = args.clone();
140                args.conversation_override = tid_override;
141                tree::run_tree(args);
142            });
143            continue;
144        }
145
146        // Default: skip (jots already parsed by parser::parse_frs)
147    }
148
149    if !stored {
150        eprintln!(
151            "{}",
152            "⚠️ Script finished without a `store` — nothing persisted.".yellow()
153        );
154    }
155}
156
157/// Run a command either with an ephemeral conversation (if not stored) or directly.
158fn with_ephemeral<F>(stored: bool, conversation: &crate::frs::ast::Thread, mut f: F)
159where
160    F: FnMut(Option<String>),
161{
162    if !stored {
163        let tid = crate::frs::persist::persist_ephemeral(conversation);
164        f(Some(tid.clone()));
165        crate::frs::persist::cleanup_ephemeral(&tid);
166    } else {
167        f(None);
168    }
169}