Skip to main content

fur_cli/commands/
run.rs

1use std::fs;
2use colored::*;
3use crate::frs::{parser, persist_frs};
4use crate::commands::{timeline, tree};
5use crate::commands::timeline::TimelineArgs;
6use crate::commands::tree::TreeArgs;
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!("⚠️ Ignoring extra `store` at line {} — already persisted", lineno + 1)
37                        .yellow()
38                        .bold()
39                );
40            }
41            continue;
42        }
43
44        // --- Status
45        if line.starts_with("status") {
46            with_ephemeral(stored, &conversation, |tid_override| {
47                let args = crate::commands::status::StatusArgs {
48                    conversation_override: tid_override,
49                };
50                crate::commands::status::run_status(args);
51            });
52            continue;
53        }
54
55        // --- Timeline
56        if line.starts_with("timeline") {
57            let parts: Vec<&str> = line.split_whitespace().collect();
58            let mut args = TimelineArgs {
59                verbose: false,
60                contents: false,
61                out: None,
62                conversation_override: None,
63            };
64            for (i, p) in parts.iter().enumerate() {
65                if *p == "--out" {
66                    args.out = parts.get(i + 1).map(|s| s.to_string());
67                }
68                if *p == "--contents" {
69                    args.contents = true;
70                }
71            }
72
73            with_ephemeral(stored, &conversation, |tid_override| {
74                let mut args = args.clone();
75                args.conversation_override = tid_override;
76                timeline::run_timeline(args);
77            });
78            continue;
79        }
80
81        // --- Printed
82        if line.starts_with("printed") {
83            let parts: Vec<&str> = line.split_whitespace().collect();
84            let mut out: Option<String> = None;
85            let mut verbose = false;
86
87            for (i, p) in parts.iter().enumerate() {
88                if *p == "--out" {
89                    out = parts.get(i + 1).map(|s| s.to_string());
90                }
91                if *p == "--verbose" || *p == "-v" {
92                    verbose = true;
93                }
94            }
95
96            with_ephemeral(stored, &conversation, |tid_override| {
97                // Load index.json so we can temporarily override active thread
98                let index_path = std::path::Path::new(".fur").join("index.json");
99                let mut index_json: serde_json::Value =
100                    serde_json::from_str(&std::fs::read_to_string(&index_path).unwrap()).unwrap();
101
102                let original_active = index_json["active_thread"].as_str().map(|s| s.to_string());
103
104                if let Some(tid) = &tid_override {
105                    index_json["active_thread"] = tid.clone().into();
106                    std::fs::write(&index_path, serde_json::to_string_pretty(&index_json).unwrap()).unwrap();
107                }
108
109                // Now run printed, which will read this modified active_thread
110                crate::commands::printed::run_printed(out.clone(), verbose);
111
112                // Restore original active_thread
113                if let Some(orig) = original_active {
114                    index_json["active_thread"] = orig.into();
115                    std::fs::write(&index_path, serde_json::to_string_pretty(&index_json).unwrap()).unwrap();
116                }
117            });
118
119            continue;
120        }
121
122
123        // --- Tree
124        if line.starts_with("tree") {
125            let args = TreeArgs { conversation_override: None };
126            with_ephemeral(stored, &conversation, |tid_override| {
127                let mut args = args.clone();
128                args.conversation_override = tid_override;
129                tree::run_tree(args);
130            });
131            continue;
132        }
133
134        // Default: skip (jots already parsed by parser::parse_frs)
135    }
136
137    if !stored {
138        eprintln!("{}", "⚠️ Script finished without a `store` — nothing persisted.".yellow());
139    }
140}
141
142/// Run a command either with an ephemeral conversation (if not stored) or directly.
143fn with_ephemeral<F>(stored: bool, conversation: &crate::frs::ast::Thread, mut f: F)
144where
145    F: FnMut(Option<String>),
146{
147    if !stored {
148        let tid = crate::frs::persist::persist_ephemeral(conversation);
149        f(Some(tid.clone()));
150        crate::frs::persist::cleanup_ephemeral(&tid);
151    } else {
152        f(None);
153    }
154}