Skip to main content

rl_lang/repl/
command_handler.rs

1//! REPL command dispatcher - handles all `:` prefixed commands.
2//!
3//! # Commands
4//!
5//! | Command              | Description                                   |
6//! |----------------------|-----------------------------------------------|
7//! | `:help`              | Print all available commands                  |
8//! | `:stdlib`            | List all stdlib modules                       |
9//! | `:stdlib <mod>`      | List all functions in a stdlib module         |
10//! | `:save <file>`       | Save all `ValidInput` lines to a file         |
11//! | `:load <file>`       | Print a file's contents into the output       |
12//! | `:attach <file>`     | Lex, parse, and evaluate a file into the env  |
13//! | `:detach <file>`     | Remove a file from the attached list          |
14//! | `:exit`              | Exit the REPL                                 |
15//!
16//! Note: `:detach` removes the file from the tracked list but does **not**
17//! undefine variables or functions already loaded into the evaluator environment.
18
19use std::{fs, path::PathBuf};
20
21use ratatui::style::{Color, Modifier, Style};
22
23use crate::{
24    interpreter::evaluator::Evaluator,
25    lexer::tokenizer::Tokenizer,
26    parser::parser_logic::Parser,
27    repl::{lines_types::OutputLine, utils::push_error},
28    utils::source::SourceFile,
29};
30
31/// Dispatches a `:command` string, mutating `output`, `evaluator`, and `attached` as needed.
32pub fn handle_command(
33    cmd: &str,
34    output: &mut Vec<OutputLine>,
35    evaluator: &mut Evaluator,
36    attached: &mut Vec<PathBuf>,
37) {
38    let parts: Vec<&str> = cmd.splitn(2, ' ').collect();
39    match parts[0] {
40        ":help" => {
41            let cmd = Style::default()
42                .fg(Color::Cyan)
43                .add_modifier(Modifier::BOLD);
44            let arg = Style::default()
45                .fg(Color::LightBlue)
46                .add_modifier(Modifier::ITALIC);
47            let sep = Style::default().fg(Color::DarkGray);
48            let desc = Style::default().fg(Color::White);
49
50            output.push(OutputLine::Styled(vec![(
51                "Commands".to_string(),
52                Style::default()
53                    .fg(Color::Cyan)
54                    .add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
55            )]));
56            let entries: &[(&str, Option<&str>, &str)] = &[
57                (":help", None, "show this"),
58                (":stdlib", None, "list stdlib modules"),
59                (":stdlib", Some(" <mod>"), "list module functions"),
60                (":save", Some(" <file>"), "save session to file"),
61                (":load", Some(" <file>"), "load and print file"),
62                (":attach", Some(" <file>"), "import file into env"),
63                (":detach", Some(" <file>"), "remove attached file"),
64                (":exit", None, "quit  (ctrl+c also works)"),
65            ];
66            for (command, argument, description) in entries {
67                let mut parts = vec![("  ".to_string(), sep), (command.to_string(), cmd)];
68                if let Some(a) = argument {
69                    parts.push((a.to_string(), arg));
70                }
71                // pad to column 24
72                let used = 2 + command.len() + argument.map(|a| a.len()).unwrap_or(0);
73                let pad = " ".repeat(24_usize.saturating_sub(used));
74                parts.push((pad, sep));
75                parts.push(("- ".to_string(), sep));
76                parts.push((description.to_string(), desc));
77                output.push(OutputLine::Styled(parts));
78            }
79        }
80        ":stdlib" => {
81            let header = Style::default()
82                .fg(Color::Cyan)
83                .add_modifier(Modifier::BOLD | Modifier::UNDERLINED);
84            let modname = Style::default()
85                .fg(Color::Cyan)
86                .add_modifier(Modifier::BOLD);
87            let sig = Style::default()
88                .fg(Color::LightBlue)
89                .add_modifier(Modifier::ITALIC);
90            let sep = Style::default().fg(Color::DarkGray);
91            let desc = Style::default().fg(Color::White);
92
93            if parts.len() == 1 {
94                output.push(OutputLine::Styled(vec![(
95                    "stdlib modules".to_string(),
96                    header,
97                )]));
98                for entry in crate::docs::entries::stdlib_entries() {
99                    output.push(OutputLine::Styled(vec![
100                        ("  std".to_string(), sep),
101                        ("::".to_string(), sep),
102                        (entry.name.to_string(), modname),
103                    ]));
104                }
105            } else {
106                let mod_name = parts[1].trim();
107                let entries = crate::docs::entries::stdlib_entries();
108                if let Some(entry) = entries.iter().find(|e| e.name == mod_name) {
109                    output.push(OutputLine::Styled(vec![
110                        ("std".to_string(), sep),
111                        ("::".to_string(), sep),
112                        (entry.name.to_string(), modname),
113                    ]));
114                    for function in entry.functions {
115                        let signature = function.signature;
116                        let description = function.description;
117                        let pad = " ".repeat(32_usize.saturating_sub(signature.len()));
118                        output.push(OutputLine::Styled(vec![
119                            ("  ".to_string(), sep),
120                            (signature.to_string(), sig),
121                            (pad, sep),
122                            (description.to_string(), desc),
123                        ]));
124                    }
125                } else {
126                    output.push(OutputLine::Error(format!("unknown module: {}", mod_name)));
127                }
128            }
129        }
130        ":save" => {
131            if parts.len() < 2 || parts[1].trim().is_empty() {
132                output.push(OutputLine::Error(":save requires a filename".into()));
133                return;
134            }
135            let path = parts[1].trim();
136            let content: String = output
137                .iter()
138                .filter_map(|l| match l {
139                    OutputLine::ValidInput(s) => Some(s.clone()),
140                    _ => None,
141                })
142                .collect::<Vec<_>>()
143                .join("\n");
144            match fs::write(path, content) {
145                Ok(_) => output.push(OutputLine::Info(format!("saved to {}", path))),
146                Err(e) => output.push(OutputLine::Error(format!("save failed: {}", e))),
147            }
148        }
149
150        ":load" => {
151            if parts.len() < 2 || parts[1].trim().is_empty() {
152                output.push(OutputLine::Error(":load requires a filename".into()));
153                return;
154            }
155            let path = parts[1].trim();
156            match fs::read_to_string(path) {
157                Ok(content) => {
158                    output.push(OutputLine::Info(format!("--- {} ---", path)));
159                    for line in content.lines() {
160                        output.push(OutputLine::Info(line.to_string()));
161                    }
162                }
163                Err(e) => output.push(OutputLine::Error(format!("load failed: {}", e))),
164            }
165        }
166
167        ":attach" => {
168            if parts.len() < 2 || parts[1].trim().is_empty() {
169                output.push(OutputLine::Error(":attach requires a filename".into()));
170                return;
171            }
172            let path = PathBuf::from(parts[1].trim());
173            match fs::read_to_string(&path) {
174                Ok(content) => {
175                    let source =
176                        SourceFile::new(path.to_str().unwrap_or("<file>"), content.clone());
177                    let tokens = match Tokenizer::lex(source.clone()) {
178                        Ok(t) => t,
179                        Err(e) => {
180                            push_error(output, &e);
181                            return;
182                        }
183                    };
184                    let (_file_ast, stmts) = match Parser::parse(tokens, source.clone()) {
185                        Ok(s) => s,
186                        Err(e) => {
187                            push_error(output, &e);
188                            return;
189                        }
190                    };
191                    evaluator.set_source_file(source);
192                    let mut ok = true;
193                    for stmt in &stmts {
194                        if let Err(e) = evaluator.evaluate_statement(stmt) {
195                            push_error(output, &e);
196                            ok = false;
197                            break;
198                        }
199                    }
200                    if ok {
201                        attached.push(path.clone());
202                        output.push(OutputLine::Info(format!("attached {}", path.display())));
203                    }
204                }
205                Err(e) => output.push(OutputLine::Error(format!("attach failed: {}", e))),
206            }
207        }
208        ":detach" => {
209            if parts.len() < 2 || parts[1].trim().is_empty() {
210                output.push(OutputLine::Error(":detach requires a filename".into()));
211                return;
212            }
213            let path = PathBuf::from(parts[1].trim());
214            if let Some(pos) = attached.iter().position(|p| p == &path) {
215                attached.remove(pos);
216                output.push(OutputLine::Info(format!("detached {}", path.display())));
217                output.push(OutputLine::Info(
218                    "note: variables from that file remain in env until restart".into(),
219                ));
220            } else {
221                output.push(OutputLine::Error(format!(
222                    "{} is not attached",
223                    path.display()
224                )));
225            }
226        }
227
228        _ => {
229            output.push(OutputLine::Error(format!("unknown command: {}", parts[0])));
230            output.push(OutputLine::Info("type :help for commands".into()));
231        }
232    }
233}