1use rustyline::completion::FilenameCompleter;
4use rustyline::error::ReadlineError;
5use rustyline::hint::HistoryHinter;
6use rustyline::{CompletionType, Config, EditMode, Editor};
7
8use crate::colours::*;
9use super::commands::{cmd_ctx, cmd_env, cmd_help, type_of};
10use super::highlight::NarjuHelper;
11use super::{parse_all, run_form, TopLevel};
12
13pub fn banner_raw() {
15 println!();
16 println!(" {BOLD}{ROSE}narju{RESET} {DIM}λ↑↓ · Pink · Purple{RESET}");
17 println!(" {THORN}a reflective tower, dreaming in orange{RESET}");
18 println!();
19 println!(" {DIM}:help for commands · :q to quit{RESET}");
20 println!();
21}
22
23pub fn banner_purple() {
29 println!();
30 println!(
32 " {BRACK}╭─ {BOLD}{ROSE}narju · purple{RESET} {BRACK}─────────────────────────────╮{RESET}"
33 );
34 println!(
36 " {BRACK}│{RESET} {THORN}a reflective tower, having compiled itself{RESET} {BRACK}│{RESET}"
37 );
38 println!(
39 " {BRACK}│{RESET} {THORN}futamura 3 collapsed; the compiler is real{RESET} {BRACK}│{RESET}"
40 );
41 println!(
43 " {BRACK}╰──────────────────────────────────────────────╯{RESET}"
44 );
45 println!();
46 println!(" {BLOOM}bring me to life{RESET} {DIM}·{RESET} {DIM}^D{RESET}");
47 println!();
48}
49
50pub fn load_file(top: &mut TopLevel, path: &str, silent: bool) -> bool {
55 let src = match crate::io::read_source(path) {
56 Ok(s) => s,
57 Err(e) => {
58 eprintln!("{ERR}error:{RESET} could not read {ROSE}{path}{RESET}: {e}");
59 return false;
60 }
61 };
62 let forms = match parse_all(&src) {
63 Ok(f) => f,
64 Err(e) => {
65 eprintln!("{ERR}parse error{RESET} in {ROSE}{path}{RESET}:\n{DIM}{e}{RESET}");
66 return false;
67 }
68 };
69 for form in &forms {
70 match run_form(top, form) {
71 Ok(Some(r)) if !silent => {
72 println!("{PETAL}{}{RESET}", r.display());
73 }
74 Ok(_) => {}
75 Err(e) => {
76 eprintln!("{ERR}error:{RESET} {e}");
77 return false;
78 }
79 }
80 }
81 if !silent {
82 println!("{THORN}loaded {ROSE}{path}{RESET}");
83 }
84 true
85}
86
87pub fn eval_str(top: &mut TopLevel, src: &str) -> bool {
91 let forms = match parse_all(src) {
92 Ok(f) => f,
93 Err(e) => {
94 eprintln!("{ERR}parse error:{RESET} {e}");
95 return false;
96 }
97 };
98 for form in &forms {
99 match run_form(top, form) {
100 Ok(Some(r)) => println!("{PETAL}{}{RESET}", r.display()),
101 Ok(None) => {}
102 Err(e) => {
103 eprintln!("{ERR}error:{RESET} {e}");
104 return false;
105 }
106 }
107 }
108 true
109}
110
111pub fn make_prompt(level: usize) -> String {
114 if level == 0 {
115 format!("{ROSE}narju{DIM}›{RESET} ")
116 } else {
117 format!("{ROSE}narju{DIM}[{RESET}{BLOOM}{level}{DIM}]›{RESET} ")
118 }
119}
120
121fn dirs_home() -> Option<std::path::PathBuf> {
124 std::env::var_os("HOME")
125 .or_else(|| std::env::var_os("USERPROFILE"))
126 .map(std::path::PathBuf::from)
127}
128
129pub fn run_repl(top: &mut TopLevel, quiet: bool) {
138 if !quiet {
139 banner_raw();
140 }
141
142 let config = Config::builder()
143 .history_ignore_space(true)
144 .completion_type(CompletionType::List)
145 .edit_mode(EditMode::Emacs)
146 .max_history_size(1000)
147 .unwrap()
148 .build();
149
150 let helper = NarjuHelper {
151 completer: FilenameCompleter::new(),
152 hinter: HistoryHinter::new(),
153 };
154
155 let mut rl: Editor<NarjuHelper, _> =
156 Editor::with_config(config).expect("failed to create editor");
157 rl.set_helper(Some(helper));
158
159 use rustyline::{Cmd, KeyEvent};
160 rl.bind_sequence(KeyEvent::alt('p'), Cmd::HistorySearchBackward);
161 rl.bind_sequence(KeyEvent::alt('n'), Cmd::HistorySearchForward);
162
163 let hist_path = dirs_home().map(|h| h.join(".narju_lud_history"));
164 if let Some(ref p) = hist_path {
165 let _ = rl.load_history(p);
166 }
167
168 loop {
169 let prompt = make_prompt(top.ctx.level);
170 let readline = rl.readline(&prompt);
171 match readline {
172 Ok(line) => {
173 let line = line.trim_end().to_string();
174 if line.is_empty() {
175 continue;
176 }
177 rl.add_history_entry(&line).ok();
178
179 if let Some(cmd) = line.strip_prefix(':') {
180 let parts: Vec<&str> = cmd.splitn(2, ' ').collect();
181 match parts[0] {
182 "q" | "quit" => break,
183 "help" => {
184 cmd_help();
185 continue;
186 }
187 "env" => {
188 let name =
189 parts.get(1).map(|s| s.trim()).filter(|s| !s.is_empty());
190 cmd_env(top, name);
191 continue;
192 }
193 "ctx" => {
194 cmd_ctx(&top.ctx);
195 continue;
196 }
197 "reset" => {
198 *top = TopLevel::new();
199 println!(" {THORN}environment cleared{RESET}");
200 continue;
201 }
202 "load" => {
203 if let Some(path) = parts.get(1) {
204 load_file(top, path.trim(), false);
205 } else {
206 eprintln!(" {WARN}usage:{RESET} :load <file>");
207 }
208 continue;
209 }
210 other => {
211 eprintln!(
212 " {WARN}unknown command:{RESET} :{other} {DIM}(try :help){RESET}"
213 );
214 continue;
215 }
216 }
217 }
218
219 let forms = match parse_all(&line) {
220 Ok(f) => f,
221 Err(e) => {
222 eprintln!("{ERR}parse error:{RESET} {DIM}{e}{RESET}");
223 continue;
224 }
225 };
226
227 for form in &forms {
228 match run_form(top, form) {
229 Ok(Some(r)) => {
230 let ty = type_of(&r.val);
231 println!(
232 "{DIM}:{RESET} {THORN}{ty}{RESET} {PETAL}{}{RESET}",
233 r.display()
234 );
235 }
236 Ok(None) => {}
237 Err(e) => eprintln!("{ERR}error:{RESET} {e}"),
238 }
239 }
240 }
241 Err(ReadlineError::Interrupted) => {
242 println!("{DIM}^C{RESET}");
243 continue;
244 }
245 Err(ReadlineError::Eof) => break,
246 Err(e) => {
247 eprintln!("{ERR}readline error:{RESET} {e}");
248 break;
249 }
250 }
251 }
252
253 if let Some(ref p) = hist_path {
254 let _ = rl.save_history(p);
255 }
256 println!("{THORN}🥀{RESET}");
257}