1use std::collections::HashMap;
2use std::io::{self};
3use std::sync::{mpsc, Arc, Mutex};
4use std::sync::atomic::AtomicBool;
5use std::{fs, thread};
6use std::path::{Path, PathBuf};
7use std::time::{Duration, Instant};
8
9use crossterm::{
10 event::{
11 self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers,
12 },
13 execute,
14 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
15};
16use ratatui::prelude::*;
17use ratatui::{backend::CrosstermBackend, layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, text::{Line, Span}, widgets::{Block, Borders, Paragraph, Wrap}, Frame, Terminal};
18use ratatui::widgets::{Cell, Clear, Row, Table};
19use crate::{BrainfuckReader, BrainfuckReaderError, bf_only};
20use crate::reader::StepControl;
21use crate::config::colors;
22
23#[derive(Copy, Clone, Debug, PartialEq, Eq)]
24enum Focus {
25 Editor,
26 Output,
27 Tape,
28}
29
30#[derive(Copy, Clone, Debug, PartialEq, Eq)]
31enum OutputMode {
32 Raw,
33 Escaped,
34}
35
36#[derive(Copy, Clone, Debug, PartialEq, Eq)]
38enum ViMode {
39 Insert,
40 Normal,
41}
42
43#[derive(Debug)]
45enum RunnerMsg {
46 Output(Vec<u8>),
48 Tape { ptr: usize, base: usize, window: [u8; 128] },
50 NeedsInput,
52 Halted(Result<(), BrainfuckReaderError>),
54}
55
56#[derive(Debug)]
57enum UiCmd {
58 ProvideInput(Option<u8>),
60 Stop,
62}
63
64struct RunnerHandle {
65 tx_cmd: mpsc::Sender<UiCmd>,
67 rx_msg: mpsc::Receiver<RunnerMsg>,
69 cancel: Arc<AtomicBool>,
71 }
73
74pub struct App {
75 buffer: Vec<String>,
77 cursor_row: usize,
78 cursor_col: usize,
79 scroll_row: usize,
80
81 output: Vec<u8>,
83
84 tape_ptr: usize,
86 tape_window_base: usize,
87 tape_window: [u8; 128],
88
89 focused: Focus,
91 dirty: bool,
92 filename: Option<String>,
93 running: bool,
94 output_mode: OutputMode,
95
96 show_help: bool,
98
99 last_tick: Instant,
101
102 runner: Option<RunnerHandle>,
104
105 show_save_dialog: bool,
107 save_name_input: String,
108 save_error: Option<String>,
109
110 show_open_dialog: bool,
112 open_name_input: String,
113 open_error: Option<String>,
114
115 show_confirm_dialog: bool,
117 confirm_message: String,
118 confirm_pending_open: Option<PathBuf>,
119
120 show_input_dialog: bool,
122 input_buffer: String,
123 input_error: Option<String>,
124
125 show_line_numbers: bool,
127
128 status_message: Option<(String, Instant)>,
130
131 vi_enabled: bool,
133 vi_mode: ViMode,
134 vi_pending_op: Option<char>,
135
136 should_quit: bool,
138 confirm_pending_quit: bool,
139
140 confirm_pending_new: bool,
142}
143
144impl Default for App {
145 fn default() -> Self {
146 Self {
147 buffer: vec![String::new()],
148 cursor_row: 0,
149 cursor_col: 0,
150 scroll_row: 0,
151 output: Vec::new(),
152 tape_ptr: 0,
153 tape_window_base: 0,
154 tape_window: [0u8; 128],
155 focused: Focus::Editor,
156 dirty: false,
157 filename: None,
158 running: false,
159 output_mode: OutputMode::Raw,
160 show_help: false,
161 last_tick: Instant::now(),
162 runner: None,
163
164 show_save_dialog: false,
165 save_name_input: String::new(),
166 save_error: None,
167
168 show_open_dialog: false,
169 open_name_input: String::new(),
170 open_error: None,
171
172 show_confirm_dialog: false,
173 confirm_message: String::new(),
174 confirm_pending_open: None,
175
176 show_input_dialog: false,
177 input_buffer: String::new(),
178 input_error: None,
179
180 show_line_numbers: true,
181
182 status_message: None,
183
184 vi_enabled: false,
185 vi_mode: ViMode::Insert,
186 vi_pending_op: None,
187
188 should_quit: false,
189 confirm_pending_quit: false,
190
191 confirm_pending_new: false,
192 }
193 }
194}
195
196pub fn run() -> io::Result<()> {
197 run_with_file(None)
199}
200
201pub fn run_with_file(initial_file: Option<PathBuf>) -> io::Result<()> {
203 run_with_options(initial_file, false)
204}
205
206pub fn run_with_options(initial_file: Option<PathBuf>, vi_enabled: bool) -> io::Result<()> {
207 enable_raw_mode()?;
209 let mut stdout = io::stdout();
210 execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
211 let backend = CrosstermBackend::new(stdout);
212 let mut terminal = Terminal::new(backend)?;
213 terminal.clear()?;
214
215 let res = run_app(&mut terminal, initial_file, vi_enabled);
216
217 disable_raw_mode()?;
219 execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
220 terminal.show_cursor()?;
221
222 res
223}
224
225fn run_app(
226 terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
227 initial_file: Option<PathBuf>,
228 vi_enabled: bool,
229) -> io::Result<()> {
230 let mut app = App::default();
231 app.vi_enabled = vi_enabled;
232 app.vi_mode = if vi_enabled { ViMode::Normal } else { ViMode::Insert };
233 let tick_rate = Duration::from_millis(33);
234
235 if let Some(path) = initial_file {
237 if let Err(err) = app_open_file(&mut app, &path) {
238 set_status(&mut app, &format!("Failed to open {}: {}", path.display(), err));
240 eprintln!("Failed to open {}: {}", path.display(), err);
241 }
242 }
243
244 loop {
245 terminal.draw(|f| ui(f, &app))?;
246
247 let timeout = tick_rate
248 .checked_sub(app.last_tick.elapsed())
249 .unwrap_or(Duration::from_secs(0));
250
251 if event::poll(timeout)? {
252 if let Event::Key(key) = event::read()? {
253 if key.kind == KeyEventKind::Press {
254 if handle_key(&mut app, key)? {
255 break;
256 }
257 }
258 }
259 }
260
261 let mut should_clear_runner = false;
262
263 let mut deferred_status: Option<String> = None;
265 let mut saw_halted: bool = false;
266
267 if let Some(handle) = app.runner.as_mut() {
269 while let Ok(msg) = handle.rx_msg.try_recv() {
270 match msg {
271 RunnerMsg::Output(bytes) => {
272 app.output.extend_from_slice(&bytes);
273 }
274 RunnerMsg::Tape { ptr, base, window } => {
275 app.tape_ptr = ptr;
276 app.tape_window_base = base;
277 app.tape_window.copy_from_slice(&window);
278 app.dirty = true;
279 }
280 RunnerMsg::NeedsInput => {
281 app.show_input_dialog = true;
283 app.input_buffer.clear();
284 app.input_error = None;
285 deferred_status = Some("Program requested input (auto-EOF sent)".to_string());
286 }
287 RunnerMsg::Halted(res) => {
288 app.running = false;
289 should_clear_runner = true;
290 saw_halted = true;
291 match res {
292 Ok(()) => {
293 deferred_status = Some("Program finished".to_string());
294 }
295 Err(e) => {
296 deferred_status = Some(format!("Error: {}", e));
297 }
298 }
299 }
300 }
301 }
302 }
303
304 if let Some(msg) = deferred_status.take() {
306 set_status(&mut app, &msg);
307 }
308
309 if should_clear_runner || saw_halted {
310 app.runner = None;
312 }
313
314 if app.last_tick.elapsed() >= tick_rate {
315 app.last_tick = Instant::now();
316
317 if let Some((_, since)) = app.status_message.as_ref() {
319 if since.elapsed() >= Duration::from_secs(5) {
320 app.status_message = None;
321 }
322 }
323 }
324
325 if app.should_quit { break; }
327 }
328
329 Ok(())
330}
331
332fn ui(f: &mut Frame, app: &App) {
333 let size = f.area();
334
335 let root = Layout::default()
337 .direction(Direction::Vertical)
338 .margin(0)
339 .constraints([Constraint::Min(1), Constraint::Length(1)].as_ref())
340 .split(size);
341
342 let main_area = root[0];
343 let status_bar = root[1];
344
345 let cols = Layout::default()
348 .direction(Direction::Horizontal)
349 .constraints([Constraint::Percentage(75), Constraint::Percentage(25)].as_ref())
350 .split(main_area);
351
352 let left = cols[0];
353 let right = cols[1];
354
355 let output_inner_lines: u16 = output_display_lines(app);
361 let output_block_height: u16 = output_inner_lines.saturating_add(2);
362 let max_output_block_height: u16 = (main_area.height / 2).max(3);
363 let desired_output_block_height: u16 = output_block_height
364 .min(max_output_block_height)
365 .max(3);
366
367 let left_rows = Layout::default()
370 .direction(Direction::Vertical)
371 .constraints([Constraint::Min(1), Constraint::Length(desired_output_block_height)].as_ref())
372 .split(left);
373
374 let editor_area = left_rows[0];
375 let output_area = left_rows[1];
376
377 draw_editor(f, editor_area, app);
378 draw_output(f, output_area, app);
379 draw_tape(f, right, app);
380 draw_status(f, status_bar, app);
381
382 if app.show_help {
383 draw_help_overlay(f, size);
384 }
385 if app.show_save_dialog {
386 draw_save_dialog(f, size, app);
387 }
388 if app.show_open_dialog {
389 draw_open_dialog(f, size, app);
390 }
391 if app.show_confirm_dialog {
392 draw_confirm_dialog(f, size, app);
393 }
394 if app.show_input_dialog {
395 draw_input_dialog(f, size, app);
396 }
397}
398
399fn draw_editor(f: &mut Frame, area: Rect, app: &App) {
400 let title = match app.filename.as_deref() {
401 Some(path) => format!("Editor - {}{}", path, if app.dirty { " *" } else { "" }),
402 None => format!("Editor - <untitled>{}", if app.dirty { " *" } else { "" },),
403 };
404 let block = Block::default()
405 .title(Span::styled(
406 title,
407 Style::default().fg(if app.focused == Focus::Editor { colors().editor_title_focused } else { colors().editor_title_unfocused }),
408 ))
409 .borders(Borders::ALL);
410
411 let inner = block.inner(area);
412 f.render_widget(block, area);
413
414 let show_ln = app.show_line_numbers;
416 let total_lines = app.buffer.len().max(1);
417 let gutter_width = if show_ln {
418 compute_gutter_width(total_lines, 3)
419 } else { 0 };
420
421 let (gutter_rect, text_rect) = if gutter_width > 0 {
423 let chunks = Layout::default()
424 .direction(Direction::Horizontal)
425 .constraints([Constraint::Length(gutter_width), Constraint::Min(1)])
426 .split(inner);
427 (Some(chunks[0]), chunks[1])
428 } else {
429 (None, inner)
430 };
431
432 let max_lines = text_rect.height as usize;
433 let start = app.scroll_row.min(app.buffer.len().saturating_sub(1));
434 let end = (start + max_lines).min(app.buffer.len());
435
436 if let Some(gut) = gutter_rect {
438 let mut glines: Vec<Line> = Vec::with_capacity(end.saturating_sub(start));
439 for (i, _) in app.buffer[start..end].iter().enumerate() {
440 let line_no = start + i + 1;
441 let s = format!("{:>width$} ", line_no, width = (gutter_width - 1) as usize);
442 glines.push(Line::from(Span::styled(s, Style::default().fg(colors().gutter_text))));
443 }
444 let gutter = Paragraph::new(glines).wrap(Wrap { trim: false });
445 f.render_widget(gutter, gut);
446 }
447
448 let mut lines: Vec<Line> = Vec::with_capacity(end.saturating_sub(start));
450 for (idx, line) in app.buffer[start..end].iter().enumerate() {
451 lines.push(highlight_bf_line(line, app, start + idx));
452 }
453
454 let paragraph = Paragraph::new(lines).wrap(Wrap { trim: false });
455 f.render_widget(paragraph, text_rect);
456
457 if app.focused == Focus::Editor {
459 let row = app
460 .cursor_row
461 .saturating_sub(app.scroll_row)
462 .min(text_rect.height.saturating_sub(1) as usize);
463 let col = app.cursor_col.min(text_rect.width.saturating_sub(1) as usize);
464 f.set_cursor_position(Position::new(text_rect.x + col as u16, text_rect.y + row as u16));
465 }
466}
467
468fn draw_output(f: &mut Frame, area: Rect, app: &App) {
469 let mode = match app.output_mode {
470 OutputMode::Raw => "Raw",
471 OutputMode::Escaped => "Esc",
472 };
473 let block = Block::default()
474 .title(Span::styled(
475 format!("Output - {mode}"),
476 Style::default().fg(if app.focused == Focus::Output { colors().output_title_focused } else { colors().output_title_unfocused }),
477 ))
478 .borders(Borders::ALL);
479 let inner = block.inner(area);
480 f.render_widget(block, area);
481
482 let paragraph = if app.output.is_empty() {
483 Paragraph::new("<no output yet>")
484 } else {
485 match app.output_mode {
486 OutputMode::Raw => {
487 let s = String::from_utf8_lossy(&app.output);
489 Paragraph::new(s.into_owned())
490 }
491 OutputMode::Escaped => {
492 let s = bytes_to_escaped(&app.output);
493 Paragraph::new(s)
494 }
495 }
496 };
497 f.render_widget(paragraph, inner);
498}
499
500fn draw_tape(f: &mut Frame, area: Rect, app: &App) {
501 let border_style = if app.focused == Focus::Tape {
502 Style::default().fg(colors().tape_border_focused)
503 } else {
504 Style::default().fg(colors().tape_border_unfocused)
505 };
506 let block = Block::default()
507 .title(Line::raw("Tape (128 cells)"))
508 .borders(Borders::ALL)
509 .border_style(border_style);
510
511 let inner = block.inner(area);
513
514 let cell_content_width: u16 = 4;
517
518 let mut cols = (inner.width / cell_content_width).max(1) as usize;
519 cols = cols.min(128);
520 if cols == 0 { cols = 1; }
521
522 let rows = ((128 + cols - 1) / cols).max(1);
523
524 let mut table_rows: Vec<Row> = Vec::with_capacity(rows);
526 for r in 0..rows {
527 let mut cells: Vec<Cell> = Vec::with_capacity(cols + 1);
528 for c in 0..cols {
529 let idx = r * cols + c;
530 if idx < 128 {
531 let byte = app.tape_window[idx];
532 let abs_idx = app.tape_window_base + idx;
533
534 let mut style = Style::default().fg(colors().tape_cell_empty).add_modifier(Modifier::BOLD);
535 if byte > 0 {
536 style = style.fg(colors().tape_cell_nonzero);
537 }
538
539 if abs_idx == app.tape_ptr {
540 style = style.fg(colors().tape_cell_pointer);
541 }
542
543 cells.push(Cell::from(format!("[{byte:02X}]")).style(style));
544 } else {
545 cells.push(Cell::from(" "));
547 }
548
549 }
550 table_rows.push(Row::new(cells));
551 }
552
553 let base_width_no_spacing = (cols as u16) * cell_content_width;
557 let leftover_no_spacing = inner.width.saturating_sub(base_width_no_spacing);
558
559 let mut constraints: Vec<Constraint> =
561 std::iter::repeat(Constraint::Length(cell_content_width))
562 .take(cols.saturating_sub(1))
563 .collect();
564
565 let last_width = cell_content_width + leftover_no_spacing;
566 constraints.push(Constraint::Length(last_width));
567
568 let table = Table::new(table_rows, constraints)
569 .block(block)
570 .column_spacing(0);
571 f.render_widget(table, area);
572}
573
574fn draw_status(f: &mut Frame, area: Rect, app: &App) {
575 let filename = app
576 .filename
577 .as_deref()
578 .unwrap_or("<untitled>");
579 let dirty = if app.dirty { "*" } else { "" };
580 let run_state = if app.running { "Running" } else { "Stopped" };
581 let output_mode = match app.output_mode {
582 OutputMode::Raw => "Raw",
583 OutputMode::Escaped => "Esc",
584 };
585 let cell_val = current_cell_value(app)
586 .map(|v| format!("{v}"))
587 .unwrap_or_else(|| "--".to_string());
588 let msg = app
589 .status_message
590 .as_ref()
591 .map(|(m, _)| m.as_str())
592 .unwrap_or("");
593 let vi_str = if app.vi_enabled {
594 match app.vi_mode {
595 ViMode::Insert => " | Vi: Insert",
596 ViMode::Normal => " | Vi: Normal",
597 }
598 } else { "" };
599
600 let status = format!(
601 " {}{} | {} | Ptr: {} | Cell: {} | Output: {}{} | {} ",
602 filename, dirty, run_state, app.tape_ptr, cell_val, output_mode, vi_str, msg
603 );
604 let block = Block::default().borders(Borders::TOP);
605 f.render_widget(block, area);
606 let inner = Rect {
607 x: area.x + 1,
608 y: area.y,
609 width: area.width.saturating_sub(2),
610 height: area.height,
611 };
612 let line = Line::from(Span::styled(status, Style::default().fg(colors().status_text)));
613 f.render_widget(Paragraph::new(line), inner);
614}
615
616fn draw_help_overlay(f: &mut Frame, area: Rect) {
617 let block = Block::default()
618 .title("Help")
619 .borders(Borders::ALL);
620
621 let w = area.width.saturating_sub(area.width / 4);
622 let h = area.height.saturating_sub(area.height / 3);
623 let x = area.x + (area.width - w) / 2;
624 let y = area.y + (area.height - h) / 2;
625 let rect = Rect { x, y, width: w, height: h };
626 f.render_widget(block, rect);
627
628 let mut text = vec![
629 Line::raw("F5/Ctrl+R: Run"),
630 Line::raw("Ctrl+N: New file Ctrl+O: Open Ctrl+S: Save"),
631 Line::raw("Tab/Shift+Tab: Switch pane focus"),
632 Line::raw("Ctrl+E: Toggle output mode (Raw/Esc)"),
633 Line::raw("F1/Ctrl+H: Toggle this help"),
634 Line::raw("Ctrl+L: Toggle line numbers"),
635 Line::raw("Ctrl+P: Jump to matching bracket, [ or ]"),
636 Line::raw(""),
637 Line::raw("Editor: Arrows, PageUp/PageDown, Home/End, typing, Enter, Backspace"),
638 Line::raw("Tape pane: [ and ] to shift window"),
639 Line::raw(""),
640 Line::raw("Input on ',': prompts for input; Esc at prompt sends EOF"),
641 Line::raw("Output Raw mode may render control bytes; switch to Escaped mode if your terminal glitches"),
642 Line::raw(""),
643 Line::raw("Ctrl+q/Esc: Quit"),
644 ];
645
646 if tui_has_vi_enabled() {
648 text.push(Line::raw(""));
649 text.push(Line::raw("Vi mode (Normal/Insert) basics:"));
650 text.push(Line::raw(" Normal: h j k l to move, 0/$ line start/end, gg/G top/end"));
651 text.push(Line::raw(" i insert, a append, o/O new line below/above"));
652 text.push(Line::raw(" x delete char, dd delete line, Esc -> Normal"));
653 }
654
655 let inner = Rect {
656 x: rect.x + 2,
657 y: rect.y + 2,
658 width: rect.width.saturating_sub(4),
659 height: rect.height.saturating_sub(4),
660 };
661
662 f.render_widget(Paragraph::new(text).wrap(Wrap { trim: false }), inner);
663}
664
665fn draw_save_dialog(f: &mut Frame, area: Rect, app: &App) {
666 let title = "Save As";
668 let prompt = "Enter file name (Esc to cancel):";
669 let input_line = format!("> {}", app.save_name_input);
670 let err_line = app.save_error.as_deref().unwrap_or("");
671
672 let mut longest = prompt.len().max(input_line.len()).max(title.len());
674 if !err_line.is_empty() {
675 longest = longest.max(err_line.len());
676 }
677
678 let horizontal_padding = 2u16;
680 let min_w = 10u16;
681 let max_w = area.width.saturating_sub(2);
682 let w = ((longest as u16) + 2 + horizontal_padding).clamp(min_w, max_w);
683
684 let base_lines = 2u16;
690 let lines = base_lines + if err_line.is_empty() { 0 } else { 1 };
691 let min_h = 4u16;
693 let max_h = area.height.saturating_sub(2);
694 let h = (lines + 2 ).clamp(min_h, max_h);
695
696 let x = area.x + (area.width.saturating_sub(w)) / 2;
698 let y = area.y + (area.height.saturating_sub(h)) / 2;
699 let rect = Rect { x, y, width: w, height: h };
700
701 f.render_widget(Clear, rect);
703
704 let block = Block::default()
705 .title(Span::styled(title, Style::default().fg(Color::White)))
706 .borders(Borders::ALL)
707 .style(Style::default().bg(Color::Black));
708 f.render_widget(block.clone(), rect);
709
710 let inner = block.inner(rect);
712
713 let left_pad = " ";
715 let mut lines: Vec<Line> = Vec::new();
716 lines.push(Line::raw(format!("{left_pad}{prompt}")));
717 lines.push(Line::raw(format!("{left_pad}{input_line}")));
718 if !err_line.is_empty() {
719 lines.push(Line::from(Span::styled(
720 format!("{left_pad}{err_line}"),
721 Style::default().fg(Color::Red),
722 )));
723 }
724
725 let paragraph = Paragraph::new(lines)
726 .wrap(Wrap { trim: false })
727 .style(Style::default().bg(Color::Black).fg(Color::White));
728 f.render_widget(paragraph, inner);
729
730 let cursor_x = inner
732 .x
733 .saturating_add(1) .saturating_add(2) .saturating_add(app.save_name_input.len() as u16)
736 .min(inner.x.saturating_add(inner.width.saturating_sub(1)));
737 let cursor_y = inner
738 .y
739 .saturating_add(1) .min(inner.y.saturating_add(area.height.saturating_sub(1)));
741 f.set_cursor_position(Position::new(cursor_x, cursor_y));
742}
743
744fn draw_open_dialog(f: &mut Frame, area: Rect, app: &App) {
745 let title = "Open File";
747 let prompt = "Enter file name to open (Esc to cancel):";
748 let input_line = format!("> {}", app.open_name_input);
749 let err_line = app.open_error.as_deref().unwrap_or("");
750
751 let mut longest = prompt.len().max(input_line.len()).max(title.len());
753 if !err_line.is_empty() {
754 longest = longest.max(err_line.len());
755 }
756
757 let horizontal_padding = 2u16;
759 let min_w = 10u16;
760 let max_w = area.width.saturating_sub(2);
761 let w = ((longest as u16) + 2 + horizontal_padding).clamp(min_w, max_w);
762
763 let base_lines = 2u16;
769 let lines = base_lines + if err_line.is_empty() { 0 } else { 1 };
770 let min_h = 4u16;
772 let max_h = area.height.saturating_sub(2);
773 let h = (lines + 2 ).clamp(min_h, max_h);
774
775 let x = area.x + (area.width.saturating_sub(w)) / 2;
777 let y = area.y + (area.height.saturating_sub(h)) / 2;
778 let rect = Rect { x, y, width: w, height: h };
779
780 f.render_widget(Clear, rect);
782
783 let block = Block::default()
784 .title(Span::styled(title, Style::default().fg(Color::White)))
785 .borders(Borders::ALL)
786 .style(Style::default().bg(Color::Black));
787 f.render_widget(block.clone(), rect);
788
789 let inner = block.inner(rect);
791
792 let left_pad = " ";
794 let mut lines_vec: Vec<Line> = Vec::new();
795 lines_vec.push(Line::raw(format!("{left_pad}{prompt}")));
796 lines_vec.push(Line::raw(format!("{left_pad}{input_line}")));
797 if !err_line.is_empty() {
798 lines_vec.push(Line::from(Span::styled(
799 format!("{left_pad}{err_line}"),
800 Style::default().fg(Color::Red),
801 )));
802 }
803
804 let paragraph = Paragraph::new(lines_vec)
805 .wrap(Wrap { trim: false })
806 .style(Style::default().bg(Color::Black).fg(Color::White));
807 f.render_widget(paragraph, inner);
808
809 let cursor_x = inner
810 .x
811 .saturating_add(1)
812 .saturating_add(2)
813 .saturating_add(app.open_name_input.len() as u16)
814 .min(inner.x.saturating_add(inner.width.saturating_sub(1)));
815 let cursor_y = inner
816 .y
817 .saturating_add(1)
818 .min(inner.y.saturating_add(area.height.saturating_sub(1)));
819 f.set_cursor_position(Position::new(cursor_x, cursor_y));
820}
821
822fn draw_confirm_dialog(f: &mut Frame, area: Rect, app: &App) {
823 let title = "Confirm";
824 let hint = "(Enter = Yes, Esc = No)";
825 let longest = title.len().max(app.confirm_message.len()).max(hint.len());
826
827 let horizontal_padding = 2u16;
828 let min_w = 20u16;
829 let max_w = area.width.saturating_sub(2);
830 let w = ((longest as u16) + 2 + horizontal_padding).clamp(min_w, max_w);
831
832 let h = 5u16; let x = area.x + (area.width.saturating_sub(w)) / 2;
834 let y = area.y + (area.height.saturating_sub(h)) / 2;
835 let rect = Rect { x, y, width: w, height: h };
836
837 f.render_widget(Clear, rect);
838
839 let block = Block::default()
840 .title(Span::styled(title, Style::default().fg(Color::White)))
841 .borders(Borders::ALL)
842 .style(Style::default().bg(Color::Black));
843 f.render_widget(block.clone(), rect);
844
845 let inner = block.inner(rect);
846
847 let hint_centered = if (hint.len() as u16) < inner.width {
849 let pad = ((inner.width as usize).saturating_sub(hint.len())) / 2;
850 format!("{}{}", " ".repeat(pad), hint)
851 } else {
852 hint.to_string()
853 };
854
855 let lines = vec![
856 Line::raw(format!(" {}", app.confirm_message)),
857 Line::from(Span::styled(hint_centered, Style::default().fg(Color::Gray))),
858 ];
859 let paragraph = Paragraph::new(lines)
860 .wrap(Wrap { trim: false })
861 .style(Style::default().bg(Color::Black).fg(Color::White));
862 f.render_widget(paragraph, inner);
863}
864
865fn draw_input_dialog(f: &mut Frame, area: Rect, app: &App) {
866 let title = "Program input";
867 let prompt = "Type a byte and press Enter (Esc to send EOF):";
868 let input_line = format!("> {}", app.input_buffer);
869 let err_line = app.input_error.as_deref().unwrap_or("");
870
871 let mut longest = prompt.len().max(input_line.len()).max(title.len());
873 if !err_line.is_empty() {
874 longest = longest.max(err_line.len());
875 }
876 let horizontal_padding = 2u16;
877 let min_w = 24u16;
878 let max_w = area.width.saturating_sub(2);
879 let w = ((longest as u16) + 2 + horizontal_padding).clamp(min_w, max_w);
880
881 let base_lines = 2u16;
882 let lines = base_lines + if err_line.is_empty() { 0 } else { 1 };
883 let min_h = 4u16;
884 let max_h = area.height.saturating_sub(2);
885 let h = (lines + 2).clamp(min_h, max_h);
886
887 let x = area.x + (area.width.saturating_sub(w)) / 2;
888 let y = area.y + (area.height.saturating_sub(h)) / 2;
889 let rect = Rect { x, y, width: w, height: h };
890
891 f.render_widget(Clear, rect);
892
893 let block = Block::default()
894 .title(Span::styled(title, Style::default().fg(Color::White)))
895 .borders(Borders::ALL)
896 .style(Style::default().bg(Color::Black));
897 f.render_widget(block.clone(), rect);
898
899 let inner = block.inner(rect);
900 let left_pad = " ";
901 let mut lines_vec: Vec<Line> = Vec::new();
902 lines_vec.push(Line::raw(format!("{left_pad}{prompt}")));
903 lines_vec.push(Line::raw(format!("{left_pad}{input_line}")));
904 if !err_line.is_empty() {
905 lines_vec.push(Line::from(Span::styled(
906 format!("{left_pad}{err_line}"),
907 Style::default().fg(Color::Red),
908 )));
909 }
910
911 let paragraph = Paragraph::new(lines_vec)
912 .wrap(Wrap { trim: false })
913 .style(Style::default().bg(Color::Black).fg(Color::White));
914 f.render_widget(paragraph, inner);
915
916 let cursor_x = inner
918 .x
919 .saturating_add(1)
920 .saturating_add(2)
921 .saturating_add(app.input_buffer.len() as u16)
922 .min(inner.x.saturating_add(inner.width.saturating_sub(1)));
923 let cursor_y = inner.y.saturating_add(1);
924 f.set_cursor_position(Position::new(cursor_x, cursor_y));
925}
926
927fn handle_key(app: &mut App, key: KeyEvent) -> io::Result<bool> {
928 if app.show_save_dialog {
930 handle_save_dialog_key(app, key)?;
931 return Ok(false);
932 }
933 if app.show_open_dialog {
934 handle_open_dialog_key(app, key)?;
935 return Ok(false);
936 }
937 if app.show_confirm_dialog {
938 handle_confirm_dialog_key(app, key)?;
939 return Ok(false);
940 }
941 if app.show_input_dialog {
942 handle_input_dialog_key(app, key)?;
943 return Ok(false);
944 }
945
946 if key.modifiers.contains(KeyModifiers::CONTROL) {
948 match key.code {
949 KeyCode::Char('q') => {
950 if app.dirty {
951 app.show_confirm_dialog = true;
952 app.confirm_message = "You have unsaved changes. Quit anyway?".to_string();
953 app.confirm_pending_quit = true;
954 return Ok(false);
956 }
957 return Ok(true)
958 }, KeyCode::Char('h') | KeyCode::F(1) => {
960 app.show_help = !app.show_help;
961 return Ok(false);
962 }
963 KeyCode::Char('r') | KeyCode::F(5) => {
964 start_runner(app);
966 return Ok(false);
967 }
968 KeyCode::Char('o') => {
969 app.show_open_dialog = true;
971 app.open_error = None;
972 app.open_name_input = app.filename.clone().unwrap_or_default();
974 return Ok(false);
975 }
976 KeyCode::Char('s') => {
977 if app.filename.is_none() {
979 app.show_save_dialog = true;
980 app.save_name_input = "untitled.bf".to_string();
981 app.save_error = None;
982 } else {
983 match app_save_current(app) {
984 Ok(_) => { }
985 Err(err) => {
986 eprintln!("Save failed: {}", err);
988 }
989 }
990 }
991 return Ok(false);
992 }
993 KeyCode::Char('e') => {
994 app.output_mode = match app.output_mode {
995 OutputMode::Raw => OutputMode::Escaped,
996 OutputMode::Escaped => OutputMode::Raw,
997 };
998 return Ok(false);
999 }
1000 KeyCode::Char('n') => {
1001 if app.dirty {
1002 app.show_confirm_dialog = true;
1003 app.confirm_message = "You have unsaved changes. Discard and create new file?".to_string();
1004 app.confirm_pending_new = true;
1005 } else {
1006 app_new_file(app);
1007 }
1008 return Ok(false);
1009 }
1010 _ => {}
1011 }
1012 }
1013
1014 match key.code {
1015 KeyCode::F(1) => {
1016 app.show_help = !app.show_help;
1017 Ok(false)
1018 }
1019 KeyCode::F(5) => {
1020 start_runner(app);
1022 Ok(false)
1023 }
1024 KeyCode::Char('.') if key.modifiers.contains(KeyModifiers::CONTROL) => {
1025 if let Some(h) = app.runner.as_ref() {
1026 h.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
1027 let _ = h.tx_cmd.send(UiCmd::Stop);
1028 }
1029 app.running = false;
1030 Ok(false)
1031 }
1032 KeyCode::F(17) => {
1033 if let Some(h) = app.runner.as_ref() {
1034 h.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
1035 let _ = h.tx_cmd.send(UiCmd::Stop);
1036 }
1037 app.running = false;
1038 Ok(false)
1039 }
1040 KeyCode::Tab => {
1041 app.focused = match app.focused {
1042 Focus::Editor => Focus::Output,
1043 Focus::Output => Focus::Tape,
1044 Focus::Tape => Focus::Editor,
1045 };
1046 Ok(false)
1047 }
1048 KeyCode::BackTab => {
1049 app.focused = match app.focused {
1050 Focus::Editor => Focus::Tape,
1051 Focus::Output => Focus::Editor,
1052 Focus::Tape => Focus::Output,
1053 };
1054 Ok(false)
1055 }
1056 KeyCode::Esc => {
1057 if app.focused == Focus::Editor && app.vi_enabled {
1059 handle_editor_key_vi(app, key);
1060 return Ok(false);
1061 }
1062 if app.show_help {
1064 app.show_help = false;
1065 Ok(false)
1066 } else {
1067 if app.dirty {
1068 app.show_confirm_dialog = true;
1069 app.confirm_message = "You have unsaved changes. Quit anyway?".to_string();
1070 app.confirm_pending_quit = true;
1071 Ok(false)
1073 } else {
1074 Ok(true) }
1076 }
1077 }
1078 _ => match app.focused {
1079 Focus::Editor => {
1080 if app.vi_enabled {
1081 handle_editor_key_vi(app, key);
1082 } else {
1083 handle_editor_key(app, key);
1084 }
1085 Ok(false)
1086 },
1087 Focus::Output => Ok(false), Focus::Tape => {
1089 handle_tape_key(app, key);
1090 Ok(false)
1091 },
1092 },
1093 }
1094}
1095
1096fn handle_tape_key(app: &mut App, key: KeyEvent) {
1097 match key.code {
1098 KeyCode::Char('[') | KeyCode::Left | KeyCode::PageUp => {
1100 let page = 128usize;
1101 let new_base = app.tape_window_base.saturating_sub(page);
1102 if new_base != app.tape_window_base {
1103 app.tape_window_base = new_base;
1104 app.dirty = true;
1105 }
1106 }
1107 KeyCode::Char(']') | KeyCode::Right | KeyCode::PageDown => {
1108 let page = 128usize;
1109 app.tape_window_base = app.tape_window_base.saturating_add(page);
1111 app.dirty = true;
1112 }
1113 KeyCode::Char('c') if key.modifiers.is_empty() => {
1115 let page = 128usize;
1116 app.tape_window_base = app.tape_ptr - (app.tape_ptr % page);
1117 app.dirty = true;
1118 }
1119 _ => {}
1120 }
1121}
1122
1123fn handle_editor_key(app: &mut App, key: KeyEvent) {
1124 match key.code {
1125 KeyCode::Left => {
1126 if app.cursor_col > 0 {
1127 app.cursor_col -= 1;
1128 } else if app.cursor_row > 0 {
1129 app.cursor_row -= 1;
1130 app.cursor_col = app.buffer[app.cursor_row].len();
1131 ensure_cursor_visible(app);
1132 }
1133 }
1134 KeyCode::Right => {
1135 let len = app.buffer[app.cursor_row].len();
1136 if app.cursor_col < len {
1137 app.cursor_col += 1;
1138 } else if app.cursor_row + 1 < app.buffer.len() {
1139 app.cursor_row += 1;
1140 app.cursor_col = 0;
1141 ensure_cursor_visible(app);
1142 }
1143 }
1144 KeyCode::Up => {
1145 if app.cursor_row > 0 {
1146 app.cursor_row -= 1;
1147 app.cursor_col = app.cursor_col.min(app.buffer[app.cursor_row].len());
1148 ensure_cursor_visible(app);
1149 }
1150 }
1151 KeyCode::Down => {
1152 if app.cursor_row + 1 < app.buffer.len() {
1153 app.cursor_row += 1;
1154 app.cursor_col = app.cursor_col.min(app.buffer[app.cursor_row].len());
1155 ensure_cursor_visible(app);
1156 }
1157 }
1158 KeyCode::Home => { app.cursor_col = 0; }
1159 KeyCode::End => { app.cursor_col = app.buffer[app.cursor_row].len(); }
1160 KeyCode::PageUp => {
1161 let jump = 10usize;
1162 app.cursor_row = app.cursor_row.saturating_sub(jump);
1163 app.cursor_col = app.cursor_col.min(app.buffer[app.cursor_row].len());
1164 ensure_cursor_visible(app);
1165 }
1166 KeyCode::PageDown => {
1167 let jump = 10usize;
1168 app.cursor_row = (app.cursor_row + jump).min(app.buffer.len().saturating_sub(1));
1169 app.cursor_col = app.cursor_col.min(app.buffer[app.cursor_row].len());
1170 ensure_cursor_visible(app);
1171 }
1172 KeyCode::Enter => {
1173 let line = app.buffer[app.cursor_row].clone();
1174 let (left, right) = line.split_at(app.cursor_col);
1175 app.buffer[app.cursor_row] = left.to_string();
1176 app.buffer.insert(app.cursor_row + 1, right.to_string());
1177 app.cursor_row += 1;
1178 app.cursor_col = 0;
1179 app.dirty = true;
1180 ensure_cursor_visible(app);
1181 }
1182 KeyCode::Backspace => {
1183 if app.cursor_col > 0 {
1184 let line = &mut app.buffer[app.cursor_row];
1185 let prev_byte_idx = nth_char_to_byte_idx(line, app.cursor_col - 1);
1186 line.drain(prev_byte_idx..nth_char_to_byte_idx(line, app.cursor_col));
1187 app.cursor_col -= 1;
1188 app.dirty = true;
1189 } else if app.cursor_row > 0 {
1190 let cur = app.buffer.remove(app.cursor_row);
1191 app.cursor_row -= 1;
1192 let prev_len_chars = app.buffer[app.cursor_row].chars().count();
1193 app.buffer[app.cursor_row].push_str(&cur);
1194 app.cursor_row = prev_len_chars;
1195 app.dirty = true;
1196 ensure_cursor_visible(app);
1197 } else {
1198 }
1200 }
1201 KeyCode::Delete => {
1202 let len_chars = app.buffer[app.cursor_row].chars().count();
1203 if app.cursor_col < len_chars {
1204 let line = &mut app.buffer[app.cursor_row];
1205 let start = nth_char_to_byte_idx(line, app.cursor_col);
1206 let end = nth_char_to_byte_idx(line, app.cursor_col + 1);
1207 line.drain(start..end);
1208 app.dirty = true;
1209 } else if app.cursor_row + 1 < app.buffer.len() {
1210 let next = app.buffer.remove(app.cursor_row + 1);
1211 app.buffer[app.cursor_row].push_str(&next);
1212 app.dirty = true;
1213 }
1214 }
1215 KeyCode::Char('l') | KeyCode::Char('L') => {
1216 if key.modifiers.contains(KeyModifiers::CONTROL) {
1217 app.show_line_numbers = !app.show_line_numbers;
1218 }
1219 }
1220 KeyCode::Char('p') | KeyCode::Char('P') => {
1221 if key.modifiers.contains(KeyModifiers::CONTROL) {
1222 if !jump_to_matching_bracket(app) {
1223 set_status(app, "No matching bracket at cursor")
1224 }
1225 return;
1226 }
1227 }
1228 KeyCode::Char(ch) => {
1229 if key.modifiers.is_empty() && !ch.is_control() {
1231 app.buffer[app.cursor_row].insert(app.cursor_col, ch);
1232 app.cursor_col += 1;
1233 app.dirty = true;
1234 ensure_cursor_visible(app);
1235 }
1236 }
1237 _ => {}
1238 }
1239
1240 if app.buffer.is_empty() {
1241 app.buffer.push(String::new());
1242 app.cursor_row = 0;
1243 app.cursor_col = 0;
1244 app.scroll_row = 0;
1245 } else {
1246 app.cursor_row = app.cursor_row.min(app.buffer.len() - 1);
1247 let cur_len = app.buffer[app.cursor_row].chars().count();
1248 app.cursor_col = app.cursor_col.min(cur_len);
1249 }
1250}
1251
1252fn handle_editor_key_vi(app: &mut App, key: KeyEvent) {
1254 match app.vi_mode {
1255 ViMode::Insert => {
1256 if let KeyCode::Esc = key.code {
1258 app.vi_mode = ViMode::Normal;
1259 app.vi_pending_op = None;
1260 return;
1261 }
1262 handle_editor_key(app, key);
1263 }
1264 ViMode::Normal => {
1265 let mut consumed = false;
1267 match key.code {
1268 KeyCode::Char('i') if key.modifiers.is_empty() => {
1269 app.vi_mode = ViMode::Insert;
1270 app.vi_pending_op = None;
1271 consumed = true;
1272 }
1273 KeyCode::Char('a') if key.modifiers.is_empty() => {
1274 let len = app.buffer[app.cursor_row].chars().count();
1276 if app.cursor_col < len {
1277 app.cursor_col += 1;
1278 }
1279 app.vi_mode = ViMode::Insert;
1280 app.vi_pending_op = None;
1281 consumed = true;
1282 }
1283 KeyCode::Char('o') if key.modifiers.is_empty() => {
1284 let next_idx = app.cursor_row + 1;
1286 app.buffer.insert(next_idx, String::new());
1287 app.cursor_row = next_idx;
1288 app.cursor_col = 0;
1289 app.vi_mode = ViMode::Insert;
1290 app.dirty = true;
1291 ensure_cursor_visible(app);
1292 app.vi_pending_op = None;
1293 consumed = true;
1294 }
1295 KeyCode::Char('O') if key.modifiers.is_empty() => {
1296 let cur_idx = app.cursor_row;
1298 app.buffer.insert(cur_idx, String::new());
1299 app.cursor_col = 0;
1300 app.dirty = true;
1301 ensure_cursor_visible(app);
1302 app.vi_mode = ViMode::Insert;
1303 app.vi_pending_op = None;
1304 consumed = true;
1305 }
1306 KeyCode::Char('h') if key.modifiers.is_empty() => {
1307 move_left(app);
1308 consumed = true;
1309 }
1310 KeyCode::Char('l') if key.modifiers.is_empty() => {
1311 move_right(app);
1312 consumed = true;
1313 }
1314 KeyCode::Char('j') if key.modifiers.is_empty() => {
1315 move_down(app);
1316 consumed = true;
1317 }
1318 KeyCode::Char('k') if key.modifiers.is_empty() => {
1319 move_up(app);
1320 consumed = true;
1321 }
1322 KeyCode::Char('x') if key.modifiers.is_empty() => {
1323 let len_chars = app.buffer[app.cursor_row].chars().count();
1325 if app.cursor_col < len_chars {
1326 let line = &mut app.buffer[app.cursor_row];
1327 let start = nth_char_to_byte_idx(line, app.cursor_col);
1328 let end = nth_char_to_byte_idx(line, app.cursor_col + 1);
1329 line.drain(start..end);
1330 app.dirty = true;
1331 }
1332 consumed = true;
1333 }
1334 KeyCode::Char('0') if key.modifiers.is_empty() => {
1335 app.cursor_col = 0;
1336 ensure_cursor_visible(app);
1337 consumed = true;
1338 }
1339 KeyCode::Char('$') if key.modifiers.is_empty() => {
1340 app.cursor_col = app.buffer[app.cursor_row].chars().count();
1341 ensure_cursor_visible(app);
1342 consumed = true;
1343 }
1344 KeyCode::Char('d') if key.modifiers.is_empty() => {
1345 if matches!(app.vi_pending_op, Some('d')) {
1346 app.vi_pending_op = None;
1348 delete_current_line(app);
1349 } else {
1350 app.vi_pending_op = Some('d');
1352 }
1353 consumed = true;
1354 }
1355 KeyCode::Char('g') if key.modifiers.is_empty() => {
1356 if matches!(app.vi_pending_op, Some('g')) {
1357 app.cursor_row = 0;
1359 app.cursor_col = 0;
1360 ensure_cursor_visible(app);
1361 app.vi_pending_op = None;
1362 } else {
1363 app.vi_pending_op = Some('g');
1365 }
1366 consumed = true;
1367 }
1368 KeyCode::Char('G') if key.modifiers.is_empty() => {
1369 app.cursor_row = app.buffer.len().saturating_sub(1);
1371 app.cursor_col = app.buffer[app.cursor_row].chars().count();
1372 ensure_cursor_visible(app);
1373 consumed = true;
1374 }
1375 KeyCode::Char('p') | KeyCode::Char('P') => {
1376 if key.modifiers.contains(KeyModifiers::CONTROL) {
1377 if !jump_to_matching_bracket(app) {
1378 set_status(app, "No matching bracket at cursor")
1379 }
1380 consumed = true;
1381 }
1382 }
1383 KeyCode::Enter => {
1384 consumed = true;
1386 }
1387 KeyCode::Esc => {
1388 app.vi_pending_op = None;
1390 consumed = true;
1391 }
1392 _ => {}
1393 }
1394 if !consumed {
1395 app.vi_pending_op = None;
1397 }
1398 }
1399 }
1400
1401 if app.buffer.is_empty() {
1403 app.buffer.push(String::new());
1404 app.cursor_row = 0;
1405 app.cursor_col = 0;
1406 app.scroll_row = 0;
1407 } else {
1408 app.cursor_row = app.cursor_row.min(app.buffer.len() - 1);
1409 let cur_len = app.buffer[app.cursor_row].chars().count();
1410 app.cursor_col = app.cursor_col.min(cur_len);
1411 }
1412}
1413
1414fn handle_save_dialog_key(app: &mut App, key: KeyEvent) -> io::Result<()> {
1415 match key.code {
1416 KeyCode::Esc => {
1417 app.show_save_dialog = false;
1418 app.save_error = None;
1419 }
1420 KeyCode::Enter => {
1421 let name = app.save_name_input.trim().to_string();
1422 if name.is_empty() {
1423 app.save_error = Some("File name cannot be empty".to_string());
1424 } else {
1425 match save_to_filename(app, &name) {
1426 Ok(_) => {
1427 set_status(app, &format!("Saved {}", name));
1428 app.show_save_dialog = false;
1429 app.save_error = None;
1430 }
1431 Err(err) => {
1432 app.save_error = Some(format!("Save failed: {}", err));
1433 set_status(app, "Save failed");
1434 }
1435 }
1436 }
1437 }
1438 KeyCode::Backspace => {
1439 app.save_name_input.pop();
1440 }
1441 KeyCode::Char(ch) => {
1442 if key.modifiers.is_empty() && !ch.is_control() {
1443 app.save_name_input.push(ch);
1444 }
1445 }
1446 _ => {}
1447 }
1448 Ok(())
1449}
1450
1451fn handle_open_dialog_key(app: &mut App, key: KeyEvent) -> io::Result<()> {
1452 match key.code {
1453 KeyCode::Esc => {
1454 app.show_open_dialog = false;
1455 app.open_error = None;
1456 }
1457 KeyCode::Enter => {
1458 let name = app.open_name_input.trim().to_string();
1459 if name.is_empty() {
1460 app.open_error = Some("Path cannot be empty".to_string());
1461 } else {
1462 let mut path = PathBuf::from(&name);
1464 if path.is_relative() {
1465 path = std::env::current_dir()?.join(path);
1466 }
1467
1468 if app.dirty {
1470 app.confirm_message = "You have unsaved changes. Open anyway? Unsaved changes will be lost.".to_string();
1471 app.confirm_pending_open = Some(path);
1472 app.show_open_dialog = false;
1473 app.show_confirm_dialog = true;
1474 } else {
1475 match app_open_file(app, &path) {
1476 Ok(_) => {
1477 app.show_open_dialog = false;
1478 app.open_error = None;
1479 }
1480 Err(err) => {
1481 app.open_error = Some(format!("Open failed: {}", err));
1482 set_status(app, "Open failed");
1483 }
1484 }
1485 }
1486 }
1487 }
1488 KeyCode::Backspace => {
1489 app.open_name_input.pop();
1490 }
1491 KeyCode::Char(ch) => {
1492 if key.modifiers.is_empty() && !ch.is_control() {
1493 app.open_name_input.push(ch);
1494 }
1495 }
1496 _ => {}
1497 }
1498 Ok(())
1499}
1500
1501fn handle_confirm_dialog_key(app: &mut App, key: KeyEvent) -> io::Result<()> {
1502 match key.code {
1503 KeyCode::Enter => {
1504 if let Some(path) = app.confirm_pending_open.take() {
1505 match app_open_file(app, &path) {
1507 Ok(_) => {
1508 app.show_confirm_dialog = false;
1509 app.open_error = None;
1510 app.show_confirm_dialog = false;
1511 }
1512 Err(err) => {
1513 app.open_error = Some(format!("Open failed: {}", err));
1514 app.show_confirm_dialog = false;
1515 app.show_open_dialog = true;
1516 set_status(app, "Open failed");
1517 }
1518 }
1519 } else if app.confirm_pending_quit {
1520 app.confirm_pending_quit = false;
1522 app.show_confirm_dialog = false;
1524 app.should_quit = true;
1526 } else if app.confirm_pending_new {
1527 app.confirm_pending_new = false;
1529 app.show_confirm_dialog = false;
1530 app_new_file(app);
1531 } else {
1532 app.show_confirm_dialog = false;
1534 }
1535
1536 }
1537 KeyCode::Esc => {
1538 app.show_confirm_dialog = false;
1540 if app.confirm_pending_open.is_some() {
1541 app.show_open_dialog = true;
1542 }
1543 app.confirm_pending_open = None;
1546 app.confirm_pending_quit = false;
1547 app.confirm_pending_new = false;
1548 app.show_confirm_dialog = false;
1549 }
1550 _ => {}
1551 }
1552 Ok(())
1553}
1554
1555fn handle_input_dialog_key(app: &mut App, key: KeyEvent) -> io::Result<()> {
1556 match key.code {
1557 KeyCode::Esc => {
1558 if let Some(h) = app.runner.as_ref() {
1560 let _ = h.tx_cmd.send(UiCmd::ProvideInput(None));
1561 }
1562 app.show_input_dialog = false;
1563 app.input_error = None;
1564 set_status(app, "Sent EOF");
1565 }
1566 KeyCode::Enter => {
1567 if app.input_buffer.is_empty() {
1568 app.input_error = Some("Type a byte or Esc for EOF".to_string());
1569 } else {
1570 let mut bytes_iter = app.input_buffer.bytes();
1572 if let Some(b) = bytes_iter.next() {
1573 if let Some(h) = app.runner.as_ref() {
1574 let _ = h.tx_cmd.send(UiCmd::ProvideInput(Some(b)));
1575 }
1576 app.show_input_dialog = false;
1577 app.input_error = None;
1578 set_status(app, &format!("Sent byte: 0x{:02X}", b));
1579 } else {
1580 app.input_error = Some("Invalid input".to_string());
1581 }
1582 }
1583 }
1584 KeyCode::Backspace => {
1585 app.input_buffer.pop();
1586 }
1587 KeyCode::Char(ch) => {
1588 if key.modifiers.is_empty() && !ch.is_control() {
1589 app.input_buffer.push(ch);
1590 app.input_error = None;
1591 }
1592 }
1593 _ => {}
1594 }
1595 Ok(())
1596}
1597
1598fn nth_char_to_byte_idx(s: &str, nth: usize) -> usize {
1599 if nth == 0 {
1600 return 0;
1601 }
1602 match s.char_indices().nth(nth) {
1603 Some((i, _)) => i,
1604 None => s.len(),
1605 }
1606}
1607
1608fn ensure_cursor_visible(app: &mut App) {
1609 let margin = 3usize;
1610 if app.cursor_row < app.scroll_row.saturating_add(margin) {
1611 app.scroll_row = app.cursor_row.saturating_sub(margin);
1612 }
1613 let end = app.scroll_row + margin * 2;
1614 if app.cursor_row > end {
1615 app.scroll_row = app.cursor_row.saturating_sub(margin);
1616 }
1617}
1618
1619fn highlight_bf_line(line: &str, app: &App, row: usize) -> Line<'static> {
1621 let (match_row_col, cursor_on_bracket) = if app.focused == Focus::Editor
1622 && row == app.cursor_row
1623 && app.cursor_col < line.chars().count()
1624 {
1625 let ch = line.chars().nth(app.cursor_col).unwrap_or('\0');
1626 if ch == '[' || ch == ']' {
1627 (find_matching_bracket(app, (app.cursor_row, app.cursor_col)), true)
1628 } else {
1629 (None, false)
1630 }
1631 } else {
1632 (None, false)
1633 };
1634
1635 let mut spans: Vec<Span<'static>> = Vec::with_capacity(line.len().max(1));
1636 for (i, ch) in line.chars().enumerate() {
1637 let base = match ch {
1638 '>' => Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
1639 '<' => Style::default().fg(Color::Green).add_modifier(Modifier::BOLD),
1640 '+' => Style::default().fg(Color::LightGreen).add_modifier(Modifier::BOLD),
1641 '-' => Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
1642 '.' => Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
1643 ',' => Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD),
1644 '[' | ']' => Style::default().fg(Color::LightMagenta).add_modifier(Modifier::BOLD),
1645 _ => Style::default().fg(Color::Gray),
1646 };
1647
1648 let styled = if cursor_on_bracket && (row, i) == (app.cursor_row, app.cursor_col) {
1650 base.add_modifier(Modifier::REVERSED | Modifier::BOLD)
1651 } else if let Some((mr, mc)) = match_row_col {
1652 if (row, i) == (mr, mc) {
1653 base.add_modifier(Modifier::REVERSED)
1654 } else {
1655 base
1656 }
1657 } else {
1658 base
1659 };
1660
1661 spans.push(Span::styled(ch.to_string(), styled));
1662 }
1663
1664 if spans.is_empty() {
1665 spans.push(Span::raw(" "))
1666 }
1667 Line::from(spans)
1668}
1669
1670fn find_matching_bracket(app: &App, pos: (usize, usize)) -> Option<(usize, usize)> {
1671 let Mapping {
1672 bf_seq,
1673 orig_to_bf_idx,
1674 bf_idx_to_orig,
1675 } = build_bf_mapping(&app.buffer);
1676
1677 let bf_idx = *orig_to_bf_idx.get(&pos)?;
1678
1679 let chars: Vec<char> = bf_seq.chars().collect();
1680 let cur = *chars.get(bf_idx)?;
1681 if cur != '[' && cur != ']' {
1682 return None;
1683 }
1684
1685 if cur == '[' {
1686 let mut depth: isize = 0;
1687 for i in (bf_idx + 1)..chars.len() {
1688 match chars[i] {
1689 '[' => depth += 1,
1690 ']' => {
1691 if depth == 0 {
1692 return bf_idx_to_orig.get(&i).copied();
1693 } else {
1694 depth -= 1;
1695 }
1696 }
1697 _ => {}
1698 }
1699 }
1700 } else {
1701 let mut depth: isize = 0;
1702 let mut i = bf_idx;
1703 while i > 0 {
1704 i -= 1;
1705 match chars[i] {
1706 ']' => depth += 1,
1707 '[' => {
1708 if depth == 0 {
1709 return bf_idx_to_orig.get(&i).copied();
1710 } else {
1711 depth -= 1;
1712 }
1713 }
1714 _ => {}
1715 }
1716 }
1717 }
1718 None
1719}
1720
1721fn jump_to_matching_bracket(app: &mut App) -> bool {
1722 let line = match app.buffer.get(app.cursor_row) {
1724 Some(l) => l,
1725 None => return false,
1726 };
1727 let len_chars = line.chars().count();
1728 if app.cursor_col >= len_chars {
1729 return false;
1730 }
1731 let cur_ch = line.chars().nth(app.cursor_col).unwrap_or('\0');
1732 if cur_ch != '[' && cur_ch != ']' {
1733 return false;
1734 }
1735
1736 if let Some((r, c)) = find_matching_bracket(app, (app.cursor_row, app.cursor_col)) {
1737 app.cursor_row = r;
1738 app.cursor_col = c;
1739 ensure_cursor_visible(app);
1740 true
1741 } else { false }
1742}
1743
1744struct Mapping {
1745 bf_seq: String,
1746 orig_to_bf_idx: HashMap<(usize, usize), usize>,
1748 bf_idx_to_orig: HashMap<usize, (usize, usize)>,
1750}
1751
1752fn build_bf_mapping(lines: &[String]) -> Mapping {
1753 let mut bf_seq = String::new();
1754 let mut orig_to_bf_idx: HashMap<(usize, usize), usize> = HashMap::new();
1755 let mut bf_idx_to_orig: HashMap<usize, (usize, usize)> = HashMap::new();
1756
1757 let is_bf = |c: char| matches!(c, '>' | '<' | '+' | '-' | '.' | ',' | '[' | ']');
1758
1759 let mut idx = 0usize;
1760 for (r, line) in lines.iter().enumerate() {
1761 for (c, ch) in line.chars().enumerate() {
1762 if is_bf(ch) {
1763 bf_seq.push(ch);
1764 orig_to_bf_idx.insert((r, c), idx);
1765 bf_idx_to_orig.insert(idx, (r, c));
1766 idx += 1;
1767 }
1768 }
1769 }
1770
1771 Mapping {
1772 bf_seq,
1773 orig_to_bf_idx,
1774 bf_idx_to_orig,
1775 }
1776}
1777
1778fn start_runner(app: &mut App) {
1780 if app.runner.is_some() {
1782 return;
1783 }
1784
1785 let source = app_current_source(app);
1787 let filtered = bf_only(&source);
1788 if filtered.trim().is_empty() {
1789 set_status(app, "Nothing to run");
1791 return;
1792 }
1793
1794 let (tx_msg, rx_msg) = mpsc::channel::<RunnerMsg>();
1796 let (tx_cmd, rx_cmd) = mpsc::channel::<UiCmd>();
1797
1798 let cancel = Arc::new(AtomicBool::new(false));
1800 let cancel_for_timer = cancel.clone();
1801
1802 let timeout_ms = std::env::var("BF_TIMEOUT_MS").ok().and_then(|s| s.parse::<usize>().ok()).unwrap_or(2_000);
1804 let max_steps = std::env::var("BF_MAX_STEPS").ok().and_then(|s| s.parse::<usize>().ok());
1805
1806 let rx_cmd_shared = Arc::new(Mutex::new(rx_cmd));
1808
1809 let program = filtered.clone();
1811 thread::spawn(move || {
1812 let cancel_for_timer = cancel_for_timer.clone();
1814 let cancel_clone = cancel_for_timer.clone();
1815 thread::spawn(move || {
1816 thread::sleep(Duration::from_millis(timeout_ms as u64));
1817 cancel_clone.store(true, std::sync::atomic::Ordering::Relaxed);
1818 });
1819
1820 let mut bf = BrainfuckReader::new(program);
1822
1823 let tx_out = tx_msg.clone();
1825 bf.set_output_sink(Box::new(move |bytes: &[u8]| {
1826 let _ = tx_out.send(RunnerMsg::Output(bytes.to_vec()));
1828 }));
1829
1830 let tx_needs_input = tx_msg.clone();
1832 let rx_input = rx_cmd_shared.clone();
1833 bf.set_input_provider(Box::new(move || {
1834 let _ = tx_needs_input.send(RunnerMsg::NeedsInput);
1835 let recv_res = {
1837 let lock = rx_input.lock().expect("rx_cmd mutex poisoned");
1838 lock.recv()
1839 };
1840 match recv_res {
1841 Ok(UiCmd::ProvideInput(b)) => b,
1842 Ok(UiCmd::Stop) => None, Err(_) => None, }
1845 }));
1846
1847 bf.set_tape_observer(
1849 128, { let tx = tx_msg.clone();
1851 move |ptr, base, window| {
1852 let mut buf = [0u8; 128];
1854 buf[..window.len().min(128)].copy_from_slice(&window[..window.len().min(128)]);
1855 let _ = tx.send(RunnerMsg::Tape { ptr, base, window: buf });
1856 }
1857 }
1858 );
1859
1860 let ctrl = StepControl::new(max_steps, cancel_for_timer.clone());
1862 let res = {
1863 bf.run_with_control(ctrl)
1864 };
1865
1866 let _ = tx_msg.send(RunnerMsg::Halted(res));
1868 });
1869
1870 app.runner = Some(RunnerHandle {
1872 tx_cmd,
1873 rx_msg,
1874 cancel,
1875 });
1876 app.running = true;
1877
1878 app.output.clear();
1880 set_status(app, "Running...");
1881}
1882
1883fn app_current_source(app: &App) -> String {
1885 if app.buffer.is_empty() {
1886 String::new()
1887 } else {
1888 let mut s = String::new();
1889 for (i, line) in app.buffer.iter().enumerate() {
1890 if i > 0 {
1891 s.push('\n');
1892 }
1893 s.push_str(line);
1894 }
1895 s
1896 }
1897}
1898
1899fn bytes_to_escaped(bytes: &[u8]) -> String {
1901 let mut out = String::with_capacity(bytes.len());
1902 for &b in bytes {
1903 match b {
1904 0x20..=0x7E => out.push(b as char), b'\n' => out.push('\n'),
1906 b'\r' => out.push('\r'),
1907 b'\t' => out.push('\t'),
1908 _ => {
1909 use std::fmt::Write as _;
1910 let _ = write!(&mut out, "\\x{:02X}", b);
1911 }
1912 }
1913 }
1914 out
1915}
1916
1917fn output_display_lines(app: &App) -> u16 {
1918 if app.output.is_empty() {
1919 return 1;
1920 }
1921 let line_count = match app.output_mode {
1922 OutputMode::Raw => {
1923 let s = String::from_utf8_lossy(&app.output);
1924 let n = s.lines().count();
1925 n.max(1)
1926 }
1927 OutputMode::Escaped => {
1928 let s = bytes_to_escaped(&app.output);
1929 let n = s.lines().count();
1930 n.max(1)
1931 }
1932 };
1933
1934 line_count as u16
1935}
1936
1937fn app_open_file(app: &mut App, path: &Path) -> io::Result<()> {
1940 let content = fs::read_to_string(path)?;
1941 let mut lines: Vec<String> = content.split('\n').map(|s| s.to_string()).collect();
1943 if lines.is_empty() {
1944 lines.push(String::new());
1945 }
1946 app.buffer = lines;
1947 app.cursor_row = 0;
1948 app.cursor_col = 0;
1949 app.scroll_row = 0;
1950 app.filename = Some(path.to_string_lossy().to_string());
1951 app.dirty = false;
1952
1953 app.output.clear();
1955 app.tape_ptr = 0;
1956 app.tape_window_base = 0;
1957 app.tape_window = [0u8; 128];
1958
1959 app.cursor_row = app.buffer.len().saturating_sub(1);
1961 app.cursor_col = app.buffer[app.cursor_row].chars().count();
1962 ensure_cursor_visible(app);
1963
1964 set_status(app, &format!("Opened {}", path.display()));
1965 Ok(())
1966}
1967
1968fn app_save_current(app: &mut App) -> io::Result<()> {
1971 let filename_owned: String;
1972 let filename = match app.filename.as_deref() {
1973 Some(p) => p,
1974 None => {
1975 let new_path = generate_new_filename()?;
1977 let s = new_path.to_string_lossy().to_string();
1978 app.filename = Some(s.clone());
1979 filename_owned = s;
1980 &filename_owned
1981 }
1982 };
1983 let content = app_current_source(app);
1984 fs::write(Path::new(filename), content)?;
1986 app.dirty = false;
1987 set_status(app, &format!("Saved {}", filename));
1988 Ok(())
1989}
1990
1991fn app_new_file(app: &mut App) {
1993 app.buffer.clear();
1994 app.buffer.push(String::new());
1995 app.cursor_row = 0;
1996 app.cursor_col = 0;
1997 app.scroll_row = 0;
1998 app.filename = None;
1999 app.dirty = false;
2000
2001 app.output.clear();
2003 app.tape_ptr = 0;
2004 app.tape_window_base = 0;
2005 app.tape_window = [0u8; 128];
2006
2007 set_status(app, "New File");
2008}
2009
2010fn generate_new_filename() -> io::Result<PathBuf> {
2013 let base = std::env::current_dir()?;
2014 let stem = "untitled";
2016 let ext = "bf";
2017
2018 let candidates = {
2019 let mut p = base.clone();
2020 p.push(format!("{stem}.{ext}"));
2021 p
2022 };
2023
2024 if !candidates.exists() {
2025 return Ok(candidates);
2026 }
2027
2028 for i in 1..10_000 {
2030 let mut p = base.clone();
2031 p.push(format!("{stem}{i}.{ext}"));
2032 if !p.exists() {
2033 return Ok(p);
2034 }
2035 }
2036
2037 Err(io::Error::new(io::ErrorKind::AlreadyExists, "Unable to generate new filename"))
2039}
2040
2041fn save_to_filename(app: &mut App, name: &str) -> io::Result<()> {
2043 let mut path = PathBuf::from(name);
2044 if path.is_relative() {
2045 path = std::env::current_dir()?.join(path);
2046 }
2047 let content = app_current_source(app);
2048 fs::write(&path, content)?;
2049 app.filename = Some(path.to_string_lossy().to_string());
2050 app.dirty = false;
2051 Ok(())
2052}
2053
2054fn current_cell_value(app: &App) -> Option<u8> {
2056 let base = app.tape_window_base;
2057 let end = base.saturating_add(128);
2058 if app.tape_ptr >= base && app.tape_ptr < end {
2059 let idx = app.tape_ptr - base;
2060 Some(app.tape_window[idx])
2061 } else {
2062 None
2063 }
2064}
2065
2066fn set_status(app: &mut App, status: &str) {
2068 app.status_message = Some((status.to_string(), Instant::now()));
2069}
2070
2071fn compute_gutter_width(total_lines: usize, max_digits: usize) -> u16 {
2073 let digits = if total_lines <= 1 { 1 } else {
2074 ((total_lines as f64).log10().floor() as usize) + 1
2075 }.clamp(2, max_digits);
2076 (digits + 1) as u16 }
2078
2079fn tui_has_vi_enabled() -> bool {
2080 false
2081}
2082
2083fn move_left(app: &mut App) {
2084 if app.cursor_col > 0 {
2085 app.cursor_col -= 1;
2086 } else if app.cursor_row > 0 {
2087 app.cursor_row -= 1;
2088 app.cursor_col = app.buffer[app.cursor_row].chars().count().saturating_sub(1);
2089 }
2090 ensure_cursor_visible(app);
2091}
2092
2093fn move_right(app: &mut App) {
2094 let len = app.buffer[app.cursor_row].chars().count();
2095 if app.cursor_col + 1 < len {
2096 app.cursor_col += 1;
2097 } else if app.cursor_row + 1 < app.buffer.len() {
2098 app.cursor_row += 1;
2099 app.cursor_col = 0;
2100 } else {
2101 app.cursor_col = len;
2102 }
2103 ensure_cursor_visible(app);
2104}
2105
2106fn move_up(app: &mut App) {
2107 if app.cursor_row > 0 {
2108 app.cursor_row -= 1;
2109 app.cursor_col = app.cursor_col.min(app.buffer[app.cursor_row].chars().count());
2110 ensure_cursor_visible(app);
2111 }
2112}
2113
2114fn move_down(app: &mut App) {
2115 if app.cursor_row + 1 < app.buffer.len() {
2116 app.cursor_row += 1;
2117 app.cursor_col = app.cursor_col.min(app.buffer[app.cursor_row].chars().count());
2118 ensure_cursor_visible(app);
2119 }
2120}
2121
2122fn delete_current_line(app: &mut App) {
2123 if app.buffer.len() == 1 {
2124 if !app.buffer[0].is_empty() {
2126 app.buffer[0].clear();
2127 app.cursor_col = 0;
2128 app.dirty = true;
2129 }
2130 return;
2131 }
2132 app.buffer.remove(app.cursor_row);
2133 if app.cursor_row >= app.buffer.len() {
2134 app.cursor_row = app.buffer.len() - 1;
2135 }
2136 app.cursor_col = app.buffer[app.cursor_row].chars().count().min(app.cursor_col);
2137 app.dirty = true;
2138 ensure_cursor_visible(app);
2139}