Krolyxon
Commit a921fe1

Branches

Tags

Refs →

bench51srcuiapp.rs

903 lines · 29.6 KB · Rust

1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3use std::process::Command;
4
5use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
6use ratatui_textarea::{CursorMove, TextArea};
7
8use crate::asm::{self, AsmOutput};
9use crate::sim::{Sim, Stop, disasm};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Mode {
13 Edit,
14 Debug,
15}
16
17/// Right-hand pane in edit mode.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum OutTab {
20 Listing,
21 Hex,
22 Errors,
23}
24
25/// Focusable panels in debug mode.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Focus {
28 Source,
29 Memory,
30 Ports,
31 Serial,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum MemTab {
36 Iram,
37 Sfr,
38 Xram,
39 Code,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum PromptKind {
44 Open,
45 SaveAs,
46 /// `addr value` poke into the memory shown in the memory panel.
47 Poke,
48}
49
50pub struct Prompt {
51 pub kind: PromptKind,
52 pub input: String,
53}
54
55/// Result of the last successful assembly, which the debugger runs.
56pub struct Build {
57 pub source: Vec<String>,
58 pub out: AsmOutput,
59 pub addr_to_line: HashMap<u16, usize>,
60}
61
62impl Build {
63 /// First address emitted by `line`, if any.
64 pub fn line_addr(&self, line: usize) -> Option<u16> {
65 self.out
66 .lines
67 .get(line)
68 .filter(|l| !l.bytes.is_empty())
69 .and_then(|l| l.addr)
70 }
71}
72
73/// Instructions executed per frame (~30 fps) for each speed setting.
74pub const SPEEDS: [u32; 8] = [1, 5, 25, 100, 500, 2_000, 10_000, 40_000];
75
76pub struct App {
77 pub path: Option<PathBuf>,
78 pub editor: TextArea<'static>,
79 saved_text: String,
80 pub mode: Mode,
81 pub out_tab: OutTab,
82 pub focus: Focus,
83 pub mem_tab: MemTab,
84 pub mem_scroll: usize,
85 /// Last assembler result, even if it had errors (for diagnostics).
86 pub last_asm: Option<(String, AsmOutput)>,
87 /// Last error-free build, loaded into the simulator.
88 pub build: Option<Build>,
89 pub sim: Sim,
90 /// Breakpoints by source line.
91 pub breakpoints: HashSet<usize>,
92 pub running: bool,
93 /// The next run chunk starts a fresh run and may leave a breakpoint.
94 resume: bool,
95 pub speed: usize,
96 pub clock_mhz: f64,
97 pub status: String,
98 /// Selected line in the debug source view.
99 pub src_cursor: usize,
100 pub src_scroll: usize,
101 pub out_scroll: usize,
102 pub port_sel: (usize, u8),
103 pub serial_input: String,
104 pub prev_iram: [u8; 256],
105 pub prev_sfr: [u8; 128],
106 pub show_help: bool,
107 pub prompt: Option<Prompt>,
108 pub quit: bool,
109 quit_armed: bool,
110 /// Set by Ctrl-G; the event loop suspends the TUI and calls
111 /// `edit_externally`.
112 pub external_edit: bool,
113}
114
115impl App {
116 pub fn new(path: Option<PathBuf>, clock_mhz: f64) -> Self {
117 let mut app = Self {
118 path: None,
119 editor: TextArea::default(),
120 saved_text: String::new(),
121 mode: Mode::Edit,
122 out_tab: OutTab::Listing,
123 focus: Focus::Source,
124 mem_tab: MemTab::Iram,
125 mem_scroll: 0,
126 last_asm: None,
127 build: None,
128 sim: Sim::new(),
129 breakpoints: HashSet::new(),
130 running: false,
131 resume: false,
132 speed: 5,
133 clock_mhz,
134 status: "F1 help · F7 assemble · F5 run · F11 step".into(),
135 src_cursor: 0,
136 src_scroll: 0,
137 out_scroll: 0,
138 port_sel: (1, 0),
139 serial_input: String::new(),
140 prev_iram: [0; 256],
141 prev_sfr: [0; 128],
142 show_help: false,
143 prompt: None,
144 quit: false,
145 quit_armed: false,
146 external_edit: false,
147 };
148 match path {
149 Some(p) => app.open(&p),
150 None => app.set_text(TEMPLATE),
151 }
152 app
153 }
154
155 fn set_text(&mut self, text: &str) {
156 let lines: Vec<String> = text.lines().map(String::from).collect();
157 self.editor = TextArea::new(if lines.is_empty() {
158 vec![String::new()]
159 } else {
160 lines
161 });
162 crate::ui::draw::style_editor(&mut self.editor);
163 self.saved_text = self.text();
164 self.breakpoints.clear();
165 self.build = None;
166 self.last_asm = None;
167 self.focus = Focus::Source;
168 self.serial_input.clear();
169 }
170
171 pub fn text(&self) -> String {
172 self.editor.lines().join("\n")
173 }
174
175 pub fn dirty(&self) -> bool {
176 self.text() != self.saved_text
177 }
178
179 /// Source differs from what the simulator is running.
180 pub fn stale(&self) -> bool {
181 self.build
182 .as_ref()
183 .is_none_or(|b| b.source != self.editor.lines())
184 }
185
186 pub fn title(&self) -> String {
187 let name = self
188 .path
189 .as_ref()
190 .map_or("untitled.asm".to_string(), |p| p.display().to_string());
191 format!("{name}{}", if self.dirty() { " [+]" } else { "" })
192 }
193
194 // ---- Files ----
195
196 fn open(&mut self, path: &Path) {
197 let text = match std::fs::read_to_string(path) {
198 Ok(t) => t,
199 Err(e) => {
200 self.status = format!("cannot open {}: {e}", path.display());
201 return;
202 }
203 };
204 if path
205 .extension()
206 .is_some_and(|e| e.eq_ignore_ascii_case("hex") || e.eq_ignore_ascii_case("ihx"))
207 {
208 match asm::hex::read(&text) {
209 Ok(bytes) => {
210 let src = disassemble_image(&bytes, path);
211 self.set_text(&src);
212 self.saved_text.clear();
213 self.path = None;
214 self.assemble();
215 self.status = format!(
216 "loaded {} ({} bytes) and disassembled it",
217 path.display(),
218 bytes.len()
219 );
220 }
221 Err(e) => self.status = format!("invalid HEX file: {e}"),
222 }
223 } else {
224 self.set_text(&text);
225 self.path = Some(path.to_path_buf());
226 self.mode = Mode::Edit;
227 self.status = format!("opened {}", path.display());
228 }
229 }
230
231 fn save(&mut self, path: Option<PathBuf>) {
232 let Some(path) = path.or_else(|| self.path.clone()) else {
233 self.prompt = Some(Prompt {
234 kind: PromptKind::SaveAs,
235 input: String::new(),
236 });
237 return;
238 };
239 let mut text = self.text();
240 text.push('\n');
241 match std::fs::write(&path, text) {
242 Ok(()) => {
243 self.saved_text = self.text();
244 self.status = format!("saved {}", path.display());
245 self.path = Some(path);
246 }
247 Err(e) => self.status = format!("cannot save {}: {e}", path.display()),
248 }
249 }
250
251 /// Opens the source in `$EDITOR` and loads the result back. The
252 /// caller must have suspended the TUI. A saved file is edited in place
253 /// (unsaved changes are saved first); an untitled buffer goes through
254 /// a temporary file.
255 pub fn edit_externally(&mut self) {
256 let (path, temp) = match self.path.clone() {
257 Some(p) => {
258 if self.dirty() {
259 self.save(None);
260 if self.dirty() {
261 return; // save failed; `save` set the status
262 }
263 }
264 (p, false)
265 }
266 None => {
267 let p = std::env::temp_dir().join(format!("i8051-{}.asm", std::process::id()));
268 if let Err(e) = std::fs::write(&p, self.text() + "\n") {
269 self.status = format!("cannot write {}: {e}", p.display());
270 return;
271 }
272 (p, true)
273 }
274 };
275
276 let line = self.editor.cursor().0 + 1;
277 let result = editor_command(&path, line).status();
278 let text = std::fs::read_to_string(&path);
279 if temp {
280 let _ = std::fs::remove_file(&path);
281 }
282 match (result, text) {
283 (Err(e), _) => self.status = format!("cannot start editor: {e}"),
284 (Ok(s), _) if !s.success() => {
285 self.status = format!("editor exited with {s}; changes not loaded")
286 }
287 (Ok(_), Err(e)) => self.status = format!("cannot read {}: {e}", path.display()),
288 (Ok(_), Ok(text)) => self.reload(&text, !temp),
289 }
290 }
291
292 /// Replaces the buffer with externally edited text, keeping the cursor
293 /// and breakpoints, and reassembles if anything changed.
294 fn reload(&mut self, text: &str, from_saved_file: bool) {
295 let lines: Vec<String> = text.lines().map(String::from).collect();
296 let lines = if lines.is_empty() {
297 vec![String::new()]
298 } else {
299 lines
300 };
301 if lines == self.editor.lines() {
302 self.status = "no changes from external editor".into();
303 return;
304 }
305 let cursor = self.editor.cursor();
306 let (row, col) = (cursor.0, cursor.1);
307 self.editor = TextArea::new(lines);
308 crate::ui::draw::style_editor(&mut self.editor);
309 self.editor
310 .move_cursor(CursorMove::Jump(row as u16, col as u16));
311 if from_saved_file {
312 self.saved_text = self.text();
313 }
314 let n = self.editor.lines().len();
315 self.breakpoints.retain(|&l| l < n);
316 if self.assemble() {
317 self.status = format!("reloaded from external editor; {}", self.status);
318 }
319 }
320
321 // ---- Build & run ----
322
323 /// Assembles the editor contents. On success the program is loaded
324 /// into a freshly reset simulator.
325 pub fn assemble(&mut self) -> bool {
326 let source = self.text();
327 let out = asm::assemble(&source);
328 let ok = out.ok();
329 if ok {
330 self.sim.load_code(out.bytes());
331 self.sim.reset();
332 self.running = false;
333 self.status = format!("assembled: {} bytes, 0 errors", out.code_size());
334 self.write_outputs(&out, &source);
335 self.build = Some(Build {
336 source: self.editor.lines().to_vec(),
337 addr_to_line: out.addr_to_line(),
338 out: out.clone(),
339 });
340 self.snapshot();
341 self.follow_pc();
342 } else {
343 let first = &out.diagnostics[0];
344 self.status = format!(
345 "{} error(s); line {}: {}",
346 out.diagnostics.len(),
347 first.line + 1,
348 first.msg
349 );
350 self.editor
351 .move_cursor(CursorMove::Jump(first.line as u16, first.col as u16));
352 self.mode = Mode::Edit;
353 self.out_tab = OutTab::Errors;
354 }
355 self.out_scroll = 0;
356 self.last_asm = Some((source, out));
357 ok
358 }
359
360 /// Writes `.lst` and `.hex` next to a saved source file.
361 fn write_outputs(&mut self, out: &AsmOutput, source: &str) {
362 let Some(path) = &self.path else { return };
363 let res = std::fs::write(path.with_extension("lst"), out.listing(source))
364 .and_then(|_| std::fs::write(path.with_extension("hex"), out.intel_hex()));
365 if let Err(e) = res {
366 self.status = format!("assembled, but writing .lst/.hex failed: {e}");
367 }
368 }
369
370 /// Makes sure an up-to-date build is loaded; returns false on errors.
371 fn ensure_built(&mut self) -> bool {
372 if self.stale() && !self.assemble() {
373 return false;
374 }
375 if self.mode == Mode::Edit {
376 self.mode = Mode::Debug;
377 self.focus = Focus::Source;
378 }
379 true
380 }
381
382 pub fn bp_addrs(&self) -> HashSet<u16> {
383 let Some(b) = &self.build else {
384 return HashSet::new();
385 };
386 self.breakpoints
387 .iter()
388 .filter_map(|&l| b.line_addr(l))
389 .collect()
390 }
391
392 pub fn snapshot(&mut self) {
393 self.prev_iram = self.sim.iram;
394 self.prev_sfr = self.sim.sfr;
395 }
396
397 pub fn pc_line(&self) -> Option<usize> {
398 self.build.as_ref()?.addr_to_line.get(&self.sim.pc).copied()
399 }
400
401 /// Scrolls the source view so the PC line is visible.
402 pub fn follow_pc(&mut self) {
403 if let Some(l) = self.pc_line() {
404 self.src_cursor = l;
405 }
406 }
407
408 fn report(&mut self, stop: Stop) {
409 self.running = false;
410 self.status = match stop {
411 Stop::Breakpoint(pc) => format!("breakpoint at {pc:04X}h"),
412 Stop::ReservedOpcode { pc, opcode } => {
413 format!("reserved opcode {opcode:02X}h at {pc:04X}h — stopped")
414 }
415 Stop::PowerDown => "CPU in power-down mode (PCON.PD) — reset to continue".into(),
416 Stop::Target | Stop::Budget => format!("stopped at {:04X}h", self.sim.pc),
417 };
418 self.follow_pc();
419 }
420
421 fn step_cmd(&mut self, f: impl FnOnce(&mut Sim, &HashSet<u16>) -> Stop) {
422 if !self.ensure_built() {
423 return;
424 }
425 self.running = false;
426 self.snapshot();
427 let bps = self.bp_addrs();
428 let stop = f(&mut self.sim, &bps);
429 match stop {
430 Stop::Target => {
431 self.status = format!("PC = {:04X}h", self.sim.pc);
432 self.follow_pc();
433 }
434 Stop::Budget => self.report(Stop::Budget),
435 s => self.report(s),
436 }
437 }
438
439 fn toggle_run(&mut self) {
440 if self.running {
441 self.running = false;
442 self.status = format!("paused at {:04X}h", self.sim.pc);
443 self.follow_pc();
444 } else if self.ensure_built() {
445 self.running = true;
446 self.resume = true;
447 self.status = "running…".into();
448 }
449 }
450
451 fn run_to_cursor(&mut self) {
452 if !self.ensure_built() {
453 return;
454 }
455 let line = if self.mode == Mode::Debug {
456 self.src_cursor
457 } else {
458 self.editor.cursor().0
459 };
460 let Some(target) = self.build.as_ref().and_then(|b| b.line_addr(line)) else {
461 self.status = "no code on that line".into();
462 return;
463 };
464 self.step_cmd(|s, bps| {
465 let mut bps = bps.clone();
466 bps.insert(target);
467 match s.run(5_000_000, &bps, true) {
468 Stop::Breakpoint(pc) if pc == target => Stop::Target,
469 other => other,
470 }
471 });
472 }
473
474 fn reset(&mut self) {
475 self.sim.reset();
476 self.running = false;
477 self.snapshot();
478 self.follow_pc();
479 self.status = "reset".into();
480 }
481
482 fn toggle_breakpoint(&mut self) {
483 let line = if self.mode == Mode::Debug {
484 self.src_cursor
485 } else {
486 self.editor.cursor().0
487 };
488 if !self.breakpoints.remove(&line) {
489 self.breakpoints.insert(line);
490 }
491 }
492
493 /// Called once per frame.
494 pub fn tick(&mut self) {
495 if !self.running {
496 return;
497 }
498 self.snapshot();
499 let bps = self.bp_addrs();
500 let resume = std::mem::take(&mut self.resume);
501 match self.sim.run(SPEEDS[self.speed], &bps, resume) {
502 Stop::Budget => {
503 if let Some(l) = self.pc_line() {
504 self.src_cursor = l;
505 }
506 }
507 s => self.report(s),
508 }
509 }
510
511 // ---- Input ----
512
513 pub fn on_mouse(&mut self, m: MouseEvent) {
514 let delta: isize = match m.kind {
515 MouseEventKind::ScrollDown => 3,
516 MouseEventKind::ScrollUp => -3,
517 _ => return,
518 };
519 match self.mode {
520 Mode::Edit => self.out_scroll = self.out_scroll.saturating_add_signed(delta),
521 Mode::Debug => self.src_cursor = self.src_cursor.saturating_add_signed(delta),
522 }
523 }
524
525 pub fn on_key(&mut self, key: KeyEvent) {
526 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
527 if !(ctrl && key.code == KeyCode::Char('q')) {
528 self.quit_armed = false;
529 }
530 if self.prompt.is_some() {
531 self.on_prompt_key(key);
532 return;
533 }
534 if self.show_help {
535 self.show_help = false;
536 return;
537 }
538
539 // Global keys.
540 match (key.code, ctrl) {
541 (KeyCode::Char('q'), true) => {
542 if self.dirty() && !self.quit_armed {
543 self.quit_armed = true;
544 self.status = "unsaved changes — press Ctrl-Q again to quit".into();
545 } else {
546 self.quit = true;
547 }
548 }
549 (KeyCode::F(1), _) => self.show_help = true,
550 (KeyCode::F(2), _) | (KeyCode::Char('s'), true) => self.save(None),
551 (KeyCode::Char('g'), true) => self.external_edit = true,
552 (KeyCode::F(3), _) | (KeyCode::Char('o'), true) => {
553 self.prompt = Some(Prompt {
554 kind: PromptKind::Open,
555 input: String::new(),
556 })
557 }
558 (KeyCode::F(4), _) => match self.mode {
559 // ensure_built switches to debug mode on success.
560 Mode::Edit => {
561 self.ensure_built();
562 }
563 Mode::Debug => self.mode = Mode::Edit,
564 },
565 (KeyCode::F(5), _) => self.toggle_run(),
566 (KeyCode::F(6), _) => self.reset(),
567 (KeyCode::F(7), _) => {
568 if self.assemble() && self.mode == Mode::Edit {
569 self.out_tab = OutTab::Listing;
570 }
571 }
572 (KeyCode::F(8), _) => self.run_to_cursor(),
573 (KeyCode::F(9), _) => self.toggle_breakpoint(),
574 (KeyCode::F(10), _) => self.step_cmd(|s, b| s.step_over(5_000_000, b)),
575 (KeyCode::F(11), _) => self.step_cmd(|s, _| match s.step() {
576 Ok(()) => Stop::Target,
577 Err(e) => e,
578 }),
579 (KeyCode::F(12), _) => self.step_cmd(|s, b| s.step_out(5_000_000, b)),
580 _ => match self.mode {
581 Mode::Edit => self.on_edit_key(key),
582 Mode::Debug => self.on_debug_key(key),
583 },
584 }
585 }
586
587 fn on_edit_key(&mut self, key: KeyEvent) {
588 let alt = key.modifiers.contains(KeyModifiers::ALT);
589 match key.code {
590 // Alt-1/2/3 switch the output tab; Alt-Up/Down scroll it.
591 KeyCode::Char('1') if alt => self.out_tab = OutTab::Listing,
592 KeyCode::Char('2') if alt => self.out_tab = OutTab::Hex,
593 KeyCode::Char('3') if alt => self.out_tab = OutTab::Errors,
594 KeyCode::Up if alt => self.out_scroll = self.out_scroll.saturating_sub(1),
595 KeyCode::Down if alt => self.out_scroll += 1,
596 KeyCode::PageUp if alt => self.out_scroll = self.out_scroll.saturating_sub(20),
597 KeyCode::PageDown if alt => self.out_scroll += 20,
598 KeyCode::Esc if self.running => self.toggle_run(),
599 _ => {
600 self.editor.input(key);
601 }
602 }
603 }
604
605 fn on_debug_key(&mut self, key: KeyEvent) {
606 if self.focus == Focus::Serial {
607 match key.code {
608 KeyCode::Enter => {
609 let mut bytes = std::mem::take(&mut self.serial_input).into_bytes();
610 bytes.push(b'\r');
611 self.sim.uart_send(&bytes);
612 return;
613 }
614 KeyCode::Backspace => {
615 self.serial_input.pop();
616 return;
617 }
618 KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
619 self.serial_input.push(c);
620 return;
621 }
622 _ => {}
623 }
624 }
625
626 let page = 16usize;
627 match key.code {
628 KeyCode::Tab => self.cycle_focus(true),
629 KeyCode::BackTab => self.cycle_focus(false),
630 KeyCode::Esc => {
631 if self.running {
632 self.toggle_run();
633 } else if self.focus == Focus::Serial {
634 self.focus = Focus::Source;
635 } else {
636 self.mode = Mode::Edit;
637 }
638 }
639 KeyCode::Char('+') | KeyCode::Char('=') => {
640 self.speed = (self.speed + 1).min(SPEEDS.len() - 1)
641 }
642 KeyCode::Char('-') => self.speed = self.speed.saturating_sub(1),
643 KeyCode::Char('?') => self.show_help = true,
644 // Letter aliases for terminals that swallow function keys.
645 KeyCode::Char('r') => self.toggle_run(),
646 KeyCode::Char('s') => self.on_key(KeyEvent::from(KeyCode::F(11))),
647 KeyCode::Char('n') => self.on_key(KeyEvent::from(KeyCode::F(10))),
648 KeyCode::Char('o') => self.on_key(KeyEvent::from(KeyCode::F(12))),
649 KeyCode::Char('g') => self.run_to_cursor(),
650 KeyCode::Char('b') => self.toggle_breakpoint(),
651 KeyCode::Char('x') => self.reset(),
652 KeyCode::Char('e') => self.mode = Mode::Edit,
653 KeyCode::Char('i') => self.set_mem_tab(MemTab::Iram),
654 KeyCode::Char('f') => self.set_mem_tab(MemTab::Sfr),
655 KeyCode::Char('d') => self.set_mem_tab(MemTab::Xram),
656 KeyCode::Char('c') => self.set_mem_tab(MemTab::Code),
657 KeyCode::Char('p') => {
658 self.prompt = Some(Prompt {
659 kind: PromptKind::Poke,
660 input: String::new(),
661 });
662 }
663 _ => match self.focus {
664 Focus::Source => self.move_src(key.code, page),
665 Focus::Memory => self.move_mem(key.code, page),
666 Focus::Ports => self.move_ports(key.code),
667 Focus::Serial => {}
668 },
669 }
670 }
671
672 fn set_mem_tab(&mut self, tab: MemTab) {
673 self.mem_tab = tab;
674 self.mem_scroll = 0;
675 self.focus = Focus::Memory;
676 }
677
678 fn cycle_focus(&mut self, fwd: bool) {
679 const ORDER: [Focus; 4] = [Focus::Source, Focus::Memory, Focus::Ports, Focus::Serial];
680 let i = ORDER.iter().position(|&f| f == self.focus).unwrap_or(0);
681 let n = ORDER.len();
682 self.focus = ORDER[if fwd { (i + 1) % n } else { (i + n - 1) % n }];
683 }
684
685 fn move_src(&mut self, code: KeyCode, page: usize) {
686 let max = self
687 .build
688 .as_ref()
689 .map_or(0, |b| b.source.len().saturating_sub(1));
690 self.src_cursor = match code {
691 KeyCode::Up => self.src_cursor.saturating_sub(1),
692 KeyCode::Down => self.src_cursor + 1,
693 KeyCode::PageUp => self.src_cursor.saturating_sub(page),
694 KeyCode::PageDown => self.src_cursor + page,
695 KeyCode::Home => 0,
696 KeyCode::End => max,
697 KeyCode::Char('.') => self.pc_line().unwrap_or(self.src_cursor),
698 _ => self.src_cursor,
699 }
700 .min(max);
701 }
702
703 fn move_mem(&mut self, code: KeyCode, page: usize) {
704 let rows = match self.mem_tab {
705 MemTab::Iram => 16,
706 MemTab::Sfr => 8,
707 MemTab::Xram | MemTab::Code => 4096,
708 };
709 self.mem_scroll = match code {
710 KeyCode::Up => self.mem_scroll.saturating_sub(1),
711 KeyCode::Down => self.mem_scroll + 1,
712 KeyCode::PageUp => self.mem_scroll.saturating_sub(page),
713 KeyCode::PageDown => self.mem_scroll + page,
714 KeyCode::Home => 0,
715 KeyCode::End => rows,
716 KeyCode::Left => {
717 self.mem_tab = match self.mem_tab {
718 MemTab::Iram => MemTab::Code,
719 MemTab::Sfr => MemTab::Iram,
720 MemTab::Xram => MemTab::Sfr,
721 MemTab::Code => MemTab::Xram,
722 };
723 0
724 }
725 KeyCode::Right => {
726 self.mem_tab = match self.mem_tab {
727 MemTab::Iram => MemTab::Sfr,
728 MemTab::Sfr => MemTab::Xram,
729 MemTab::Xram => MemTab::Code,
730 MemTab::Code => MemTab::Iram,
731 };
732 0
733 }
734 _ => self.mem_scroll,
735 }
736 .min(rows.saturating_sub(1));
737 }
738
739 fn move_ports(&mut self, code: KeyCode) {
740 let (port, bit) = &mut self.port_sel;
741 match code {
742 KeyCode::Up => *port = port.saturating_sub(1),
743 KeyCode::Down => *port = (*port + 1).min(3),
744 // Bits are drawn MSB first, so Left moves towards bit 7.
745 KeyCode::Left => *bit = (*bit + 1).min(7),
746 KeyCode::Right => *bit = bit.saturating_sub(1),
747 KeyCode::Char(' ') | KeyCode::Enter => {
748 self.sim.pins[*port] ^= 1 << *bit;
749 }
750 _ => {}
751 }
752 }
753
754 fn on_prompt_key(&mut self, key: KeyEvent) {
755 let Some(p) = &mut self.prompt else { return };
756 match key.code {
757 KeyCode::Esc => self.prompt = None,
758 KeyCode::Backspace => {
759 p.input.pop();
760 }
761 KeyCode::Char(c) => p.input.push(c),
762 KeyCode::Enter => {
763 let p = self.prompt.take().unwrap();
764 let input = p.input.trim().to_string();
765 if input.is_empty() {
766 return;
767 }
768 match p.kind {
769 PromptKind::Open => self.open(Path::new(&input)),
770 PromptKind::SaveAs => self.save(Some(PathBuf::from(input))),
771 PromptKind::Poke => self.poke(&input),
772 }
773 }
774 _ => {}
775 }
776 }
777
778 /// `addr value` → write `value` at `addr` in the current memory tab.
779 fn poke(&mut self, input: &str) {
780 let parse = |s: &str| {
781 let s = s.trim_start_matches("0x").trim_end_matches(['h', 'H']);
782 u32::from_str_radix(s, 16).ok()
783 };
784 let parts: Vec<&str> = input.split_whitespace().collect();
785 let (Some(addr), Some(val)) = (
786 parts.first().and_then(|s| parse(s)),
787 parts.get(1).and_then(|s| parse(s)),
788 ) else {
789 self.status = "usage: <addr> <value> (hex), e.g. 30 FF".into();
790 return;
791 };
792 if val > 0xFF {
793 self.status = "value must be a byte".into();
794 return;
795 }
796 let v = val as u8;
797 let ok = match self.mem_tab {
798 MemTab::Iram if addr <= 0xFF => {
799 self.sim.iram[addr as usize] = v;
800 true
801 }
802 MemTab::Sfr if (0x80..=0xFF).contains(&addr) => {
803 self.sim.set_sfr(addr as u8, v);
804 true
805 }
806 MemTab::Xram if addr <= 0xFFFF => {
807 self.sim.xram[addr as usize] = v;
808 true
809 }
810 MemTab::Code if addr <= 0xFFFF => {
811 self.sim.code[addr as usize] = v;
812 true
813 }
814 _ => false,
815 };
816 self.status = if ok {
817 format!("wrote {v:02X}h at {addr:X}h")
818 } else {
819 "address out of range".into()
820 };
821 }
822}
823
824/// Command that opens `path` at `line` in the user's editor: `$EDITOR`,
825/// then `$VISUAL`, then `vi`. Run through `sh` so values such as
826/// `code --wait` work as they do for git.
827fn editor_command(path: &Path, line: usize) -> Command {
828 let editor = ["EDITOR", "VISUAL"]
829 .iter()
830 .filter_map(|v| std::env::var(v).ok())
831 .find(|v| !v.trim().is_empty())
832 .unwrap_or_else(|| "vi".into());
833 // Editors known to accept `+LINE` before the file name.
834 let program = editor.split_whitespace().next().unwrap_or_default();
835 let name = Path::new(program)
836 .file_name()
837 .and_then(|n| n.to_str())
838 .unwrap_or_default();
839 let goto = matches!(
840 name,
841 "vi" | "vim" | "nvim" | "nano" | "emacs" | "micro" | "kak" | "ne"
842 )
843 .then(|| format!("+{line}"));
844
845 let mut cmd = Command::new("sh");
846 cmd.arg("-c").arg(format!("{editor} \"$@\"")).arg("sh");
847 cmd.args(goto).arg(path);
848 cmd
849}
850
851/// Builds an assembler source from a HEX image by disassembling each
852/// contiguous run of bytes. Reassembling it reproduces the image exactly.
853fn disassemble_image(bytes: &[(u16, u8)], path: &Path) -> String {
854 let mut code = Box::new([0u8; 0x10000]);
855 let mut used = vec![false; 0x10000];
856 for &(a, b) in bytes {
857 code[a as usize] = b;
858 used[a as usize] = true;
859 }
860 let mut src = format!("; disassembled from {}\n", path.display());
861 let mut addr = 0usize;
862 while addr < 0x10000 {
863 if !used[addr] {
864 addr += 1;
865 continue;
866 }
867 src.push_str(&format!(
868 "\n ORG {}\n",
869 crate::sfr::hex16(addr as u16)
870 ));
871 while addr < 0x10000 && used[addr] {
872 let d = disasm::disassemble(&code, addr as u16);
873 let size = d.size as usize;
874 if (addr..addr + size).all(|a| a < 0x10000 && used[a]) {
875 src.push_str(&format!(" {:<24}; {:04X}\n", d.text, addr));
876 addr += size;
877 } else {
878 src.push_str(&format!(
879 " DB {:<21}; {:04X}\n",
880 crate::sfr::hex8(code[addr]),
881 addr
882 ));
883 addr += 1;
884 }
885 }
886 }
887 src.push_str(" END\n");
888 src
889}
890
891const TEMPLATE: &str = "\
892; New 8051 program. F7 assembles, F5 runs, F11 steps, F1 shows all keys.
893 ORG 0000h
894 LJMP main
895
896 ORG 0030h
897main: MOV A,#01h ; walking LED pattern on P1
898loop: MOV P1,A
899 RL A
900 SJMP loop
901
902 END
903";