1use std::{env, thread};
2use std::io::{self, IsTerminal, Write};
3use std::sync::{mpsc, Arc};
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::time::Duration;
6use reedline::{Signal, DefaultPrompt, DefaultPromptSegment, HistoryItem, Highlighter, StyledText};
7use nu_ansi_term::Style;
8use crate::{cli_util, BrainfuckReader, BrainfuckReaderError, bf_only};
9use crate::cli_util::rat_to_nu;
10use crate::reader::StepControl;
11
12pub fn repl_loop() -> io::Result<()> {
13 let mut editor = init_line_editor()?;
15
16 let mut current_buffer: String = String::new();
18
19 loop {
20 let submission = read_submission_interactive(&mut editor)?;
22 if submission.is_none() {
23 println!();
25 io::stdout().flush()?;
26 return Ok(());
27 }
28
29 let submission = submission.unwrap();
30
31 if let Some(meta) = parse_meta_command(&submission) {
33 match handle_meta_command(&mut editor, &meta, ¤t_buffer)? {
34 MetaAction::Exit => return Ok(()),
35 MetaAction::Continue => {},
36 MetaAction::ResetState => {
37 current_buffer = String::new();
39 }
40 }
41 continue; }
43
44 current_buffer = submission.clone();
46
47 let trimmed = submission.trim();
48 if trimmed.is_empty() {
49 continue; }
51
52 let filtered = bf_only(&trimmed);
53 if filtered.is_empty() {
54 continue;
55 }
56
57 execute_bf_buffer(filtered);
59
60 if env::var("BF_REPL_ONCE").ok().as_deref() == Some("1") {
62 return Ok(());
63 }
64 }
65}
66
67fn init_line_editor() -> io::Result<reedline::Reedline> {
68 use reedline::{
69 default_emacs_keybindings, EditCommand, Emacs, KeyCode, KeyModifiers, Reedline, ReedlineEvent,
70 };
71
72 let mut keybindings = default_emacs_keybindings();
77 keybindings.add_binding(KeyModifiers::NONE, KeyCode::Enter, ReedlineEvent::Edit(vec![EditCommand::InsertNewline]));
78 keybindings.add_binding(KeyModifiers::CONTROL, KeyCode::Char('d'), ReedlineEvent::Submit);
79 keybindings.add_binding(KeyModifiers::CONTROL, KeyCode::Char('z'), ReedlineEvent::Submit);
80
81 keybindings.add_binding(
84 KeyModifiers::NONE,
85 KeyCode::Up,
86 ReedlineEvent::Up
87 );
88 keybindings.add_binding(
89 KeyModifiers::NONE,
90 KeyCode::Down,
91 ReedlineEvent::Down
92 );
93
94 keybindings.add_binding(KeyModifiers::ALT, KeyCode::Up, ReedlineEvent::PreviousHistory);
97 keybindings.add_binding(KeyModifiers::CONTROL, KeyCode::Up, ReedlineEvent::PreviousHistory);
98 keybindings.add_binding(KeyModifiers::ALT, KeyCode::Down, ReedlineEvent::NextHistory);
99 keybindings.add_binding(KeyModifiers::CONTROL, KeyCode::Down, ReedlineEvent::NextHistory);
100
101 let history = reedline::FileBackedHistory::new(1_000).unwrap();
102
103 let editor = Reedline::create()
104 .with_highlighter(Box::new(BrainfuckHighlighter::new_from_config()))
105 .with_history(Box::new(history))
106 .with_edit_mode(Box::new(Emacs::new(keybindings)));
107
108 Ok(editor)
109}
110
111pub fn read_submission<R: io::BufRead>(stdin: &mut R) -> Option<String> {
112 let mut buffer = String::new();
114
115 loop {
116 let mut line = String::new();
117 match stdin.read_line(&mut line) {
118 Ok(0) => {
119 break;
121 }
122 Ok(_) => {
123 buffer.push_str(&line);
124 }
125 Err(_) => {
126 return None;
128 }
129 }
130 }
131
132 if buffer.is_empty() {
133 None
134 } else {
135 Some(buffer)
136 }
137}
138
139fn read_submission_interactive(editor: &mut reedline::Reedline) -> io::Result<Option<String>> {
140 let prompt = DefaultPrompt::new(DefaultPromptSegment::Basic("bf".to_string()), DefaultPromptSegment::Empty);
142
143 let res = editor.read_line(&prompt);
146
147 match res {
148 Ok(Signal::Success(buffer)) => {
149 if !buffer.trim().is_empty() && !buffer.trim_start().starts_with(':') {
151 let _ = editor.history_mut().save(HistoryItem::from_command_line(buffer.clone()));
152 }
153 Ok(Some(buffer))
154 }
155 Ok(Signal::CtrlC) => Ok(None), Ok(Signal::CtrlD) => Ok(None), Err(e) => {
158 eprintln!("repl: editor error: {e}");
160 let _ = io::stderr().flush();
161 Ok(None)
162 }
163 }
164
165}
166
167
168fn execute_bf_buffer(buffer: String) {
174 let timeout_ms = env::var("BF_TIMEOUT_MS").ok().and_then(|s| s.parse::<usize>().ok()).unwrap_or(2_000);
176 let max_steps = env::var("BF_MAX_STEPS").ok().and_then(|s| s.parse::<usize>().ok());
177
178 let cancel_flag = Arc::new(AtomicBool::new(false));
180 let (tx, rx) = mpsc::channel::<Result<(), BrainfuckReaderError>>();
181 let program = buffer.clone();
182 let cancel_flag_clone = cancel_flag.clone();
183
184 thread::spawn(move || {
185 let mut bf = BrainfuckReader::new(program);
186 let ctrl = StepControl::new(max_steps, cancel_flag_clone);
187 let res = bf.run_with_control(ctrl);
189 let _ = tx.send(res);
190 });
191
192 let timeout = Duration::from_millis(timeout_ms as u64);
193 match rx.recv_timeout(timeout) {
194 Ok(Ok(())) => { } Ok(Err(BrainfuckReaderError::StepLimitExceeded { limit })) => {
196 eprintln!("Execution aborted: step limit exceeded ({limit})");
197 let _ = io::stderr().flush();
198 }
199 Ok(Err(BrainfuckReaderError::Canceled)) => {
200 eprintln!("Execution aborted: wall-clock timeout ({timeout_ms} ms)");
201 let _ = io::stderr().flush();
202 }
203 Ok(Err(other)) => {
204 cli_util::print_reader_error(None, &buffer, &other);
205 let _ = io::stderr().flush();
206 }
207 Err(mpsc::RecvTimeoutError::Timeout) => {
208 cancel_flag.store(true, Ordering::Relaxed);
210 eprintln!("Execution aborted: wall-clock timeout ({} ms)", timeout_ms);
211 let _ = io::stderr().flush();
212 }
213 Err(mpsc::RecvTimeoutError::Disconnected) => {} }
215
216 println!();
217 let _ = io::stdout().flush(); }
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub enum ReplMode {
222 Bare,
223 Editor,
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227pub enum ModeFlagOverride {
228 None,
229 Bare,
230 Editor,
231}
232
233pub fn select_mode(flag: ModeFlagOverride) -> Result<ReplMode, String> {
234 match flag {
236 ModeFlagOverride::Bare => return Ok(ReplMode::Bare),
237 ModeFlagOverride::Editor => {
238 if !io::stdin().is_terminal() {
239 return Err("cannot start editor: stdin is not a TTY (use --bare or BF_REPL_MODE=bare)".to_string());
240 }
241 return Ok(ReplMode::Editor);
242 }
243 ModeFlagOverride::None => {}
244 }
245
246 if let Ok(val) = env::var("BF_REPL_MODE") {
248 let v = val.trim().to_ascii_lowercase();
249 return match v.as_str() {
250 "bare" => Ok(ReplMode::Bare),
251 "editor" => {
252 if !io::stdin().is_terminal() {
253 return Err("cannot start editor: stdin is not a TTY (use BF_REPL_MODE=bare)".to_string());
254 }
255 Ok(ReplMode::Editor)
256 }
257 _ => Err(format!("invalid BF_REPL_MODE value: {val}, must be 'bare' or 'editor'")),
258 }
259 }
260
261 if io::stdin().is_terminal() {
263 Ok(ReplMode::Editor)
264 } else {
265 Ok(ReplMode::Bare)
266 }
267}
268
269pub fn execute_bare_once() -> io::Result<()> {
270 let mut locked = io::BufReader::new(io::stdin().lock());
271 let submission = read_submission(&mut locked);
272 if let Some(s) = submission {
273 let trimmed = s.trim();
274 if !trimmed.is_empty() {
275 let filtered = bf_only(trimmed);
276 if !filtered.is_empty() {
277 execute_bf_buffer(filtered);
278 }
279 }
280 }
281 Ok(())
282}
283
284#[derive(Default)]
285struct BrainfuckHighlighter {
286 map_plus: Style,
288 map_minus: Style,
289 map_lt: Style,
290 map_gt: Style,
291 map_dot: Style,
292 map_comma: Style,
293 map_lbracket: Style,
294 map_rbracket: Style,
295 map_other: Style,
296}
297
298impl BrainfuckHighlighter {
299 fn new_from_config() -> Self {
300 use crate::config::colors;
301
302 let cfg = colors();
304 let mut s = Self::default();
305 s.map_gt = Style::new().fg(rat_to_nu(cfg.editor_op_right)).bold();
306 s.map_lt = Style::new().fg(rat_to_nu(cfg.editor_op_left)).bold();
307 s.map_plus = Style::new().fg(rat_to_nu(cfg.editor_op_inc)).bold();
308 s.map_minus = Style::new().fg(rat_to_nu(cfg.editor_op_dec)).bold();
309 s.map_dot = Style::new().fg(rat_to_nu(cfg.editor_op_output)).bold();
310 s.map_comma = Style::new().fg(rat_to_nu(cfg.editor_op_input)).bold();
311 s.map_lbracket = Style::new().fg(rat_to_nu(cfg.editor_op_bracket)).bold();
312 s.map_rbracket = Style::new().fg(rat_to_nu(cfg.editor_op_bracket)).bold();
313 s.map_other = Style::new().fg(rat_to_nu(cfg.editor_non_bf)).bold();
314 s
315 }
316
317 #[inline]
318 fn style_for(&self, ch: char) -> Style {
319 match ch {
320 '>' => self.map_gt,
321 '<' => self.map_lt,
322 '+' => self.map_plus,
323 '-' => self.map_minus,
324 '.' => self.map_dot,
325 ',' => self.map_comma,
326 '[' => self.map_lbracket,
327 ']' => self.map_rbracket,
328 _ => self.map_other,
329 }
330 }
331}
332
333impl Highlighter for BrainfuckHighlighter {
334 fn highlight(&self, line: &str, _cursor: usize) -> StyledText {
335 let mut out: StyledText = StyledText::new();
336 let mut current_style: Option<Style> = None;
337 let mut buffer = String::new();
338
339 for ch in line.chars() {
340 let style = self.style_for(ch);
341
342 match current_style {
343 None => {
344 current_style = Some(style);
345 buffer.push(ch);
346 }
347 Some(s) if s == style => {
348 buffer.push(ch);
349 }
350 Some(s) => {
351 out.push((s, std::mem::take(&mut buffer)));
352 current_style = Some(style);
353 buffer.push(ch);
354 }
355 }
356 }
357
358 if let Some(s) = current_style {
359 if !buffer.is_empty() {
360 out.push((s, buffer));
361 }
362 }
363 out
364 }
365}
366
367#[derive(Debug, Clone, PartialEq, Eq)]
368enum MetaCommand {
369 Exit,
371 Help,
373 Reset,
375 Dump {
377 with_line_numbers: bool,
378 all_to_stderr: bool,
379 },
380}
381
382#[derive(Debug, Clone, Copy, PartialEq, Eq)]
383enum MetaAction {
384 Continue,
385 Exit,
386 ResetState,
387}
388
389fn parse_meta_command(input: &str) -> Option<MetaCommand> {
390 let line = input.trim();
391 if !line.starts_with(':') {
392 return None;
393 }
394 let mut parts = line.split_whitespace();
395 let head = parts.next().unwrap_or("");
396 match head {
397 ":exit" | ":quit" => Some(MetaCommand::Exit),
398 ":help" => Some(MetaCommand::Help),
399 ":reset" => Some(MetaCommand::Reset),
400 ":dump" => {
401 let mut with_line_numbers = false;
402 let mut all_to_stderr = false;
403 for arg in parts {
404 match arg {
405 "--line-numbers" | "-n" => with_line_numbers = true,
406 "--stderr" | "-e" => all_to_stderr = true,
407 _ => {}
408 }
409 }
410 Some(MetaCommand::Dump { with_line_numbers, all_to_stderr })
411 }
412 _ => Some(MetaCommand::Help),
413 }
414}
415
416fn handle_meta_command(editor: &mut reedline::Reedline, cmd: &MetaCommand, current_buffer_snapshot: &str) -> io::Result<MetaAction> {
417 use reedline::EditCommand;
418
419 match cmd {
420 MetaCommand::Exit => Ok(MetaAction::Exit),
421 MetaCommand::Help => {
422 print_meta_help_text()?;
423 Ok(MetaAction::Continue)
424 }
425 MetaCommand::Reset => {
426 let _ = editor.run_edit_commands(&[EditCommand::Clear]);
427 eprintln!("buffer reset");
428 let _ = io::stderr().flush();
429 Ok(MetaAction::ResetState)
430 }
431 MetaCommand::Dump { with_line_numbers, all_to_stderr } => {
432 dump_buffer(current_buffer_snapshot, *with_line_numbers, *all_to_stderr)?;
433 Ok(MetaAction::Continue)
434 }
435 }
436}
437
438fn print_meta_help_text() -> io::Result<()> {
439 let mut err = io::stderr();
440 writeln!(err, "Meta commands:")?;
441 writeln!(err, " :help Show this help")?;
442 writeln!(err, " :exit Exit immediately (code 0)")?;
443 writeln!(err, " :reset Clear the current buffer")?;
444 writeln!(err, " :dump [-n|--stderr] Print the current buffer (approx: last executed)")?;
445 writeln!(err)?;
446 writeln!(err, "Editing: Enter inserts newline; Ctrl+D (or Ctrl+Z on Windows) submits the buffer")?;
447 writeln!(err, "Streams: program output -> stdout; prompts/meta/errors -> stderr")?;
448 err.flush()?;
449 Ok(())
450}
451
452fn dump_buffer(buf: &str, with_line_numbers: bool, all_to_stderr: bool) -> io::Result<()> {
453 let mut out_stdout = io::stdout();
454 let mut out_stderr = io::stderr();
455
456 let lines: Vec<&str> = if buf.is_empty() { Vec::new() } else { buf.split_inclusive("\n").collect() };
457 let line_count = if lines.is_empty() {
458 if buf.is_empty() { 0 } else { 1 }
459 } else {
460 let mut c = 0usize;
462 for l in &lines {
463 if l.ends_with('\n') {
464 c += 1;
465 }
466 }
467 if buf.ends_with('\n') { c } else { c + 1 }
468 };
469
470 if all_to_stderr {
471 writeln!(out_stderr, "- dump ({} lines) -", line_count)?;
472 write_dump_lines(&mut out_stderr, buf, with_line_numbers)?;
473 writeln!(out_stderr, "- end dump -")?;
474 out_stderr.flush()?;
475 } else {
476 writeln!(out_stderr, "- dump ({} lines) -", line_count)?;
477 write_dump_lines(&mut out_stdout, buf, with_line_numbers)?;
478 out_stdout.flush()?;
479 writeln!(out_stdout, "")?;
480 writeln!(out_stderr, "- end dump -")?;
481 out_stderr.flush()?;
482 }
483
484 Ok(())
485}
486
487fn write_dump_lines<W: Write>(mut w: W, buf: &str, with_line_numbers: bool) -> io::Result<()> {
488 if !with_line_numbers {
489 write!(w, "{}", buf)?;
490 return Ok(());
491 }
492
493 if buf.is_empty() {
495 return Ok(());
496 }
497 for (i, line) in buf.split_inclusive('\n').enumerate() {
498 write!(w, "{:>4} | {}", i + 1, line)?;
500 if !line.ends_with('\n') {
501 }
503 }
504 Ok(())
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510 use std::io::Cursor;
511
512 #[test]
513 fn read_submission_reads_until_eof_multiple_lines() {
514 let input = b"+++\n>+.\n";
515 let mut cursor = Cursor::new(&input[..]);
516 let got = read_submission(&mut cursor);
517 assert_eq!(got.as_deref(), Some("+++\n>+.\n"));
518 }
519
520 #[test]
521 fn read_submission_empty_returns_none() {
522 let mut cursor = Cursor::new(Vec::<u8>::new());
523 let got = read_submission(&mut cursor);
524 assert!(got.is_none());
525 }
526}