1use 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
31pub 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 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}