Krolyxon
Commit 6e1cd95

Branches

Tags

Refs →

bench51srcuidraw.rs

882 lines · 29.8 KB · Rust

1use ratatui::Frame;
2use ratatui::layout::{Constraint, Layout, Rect};
3use ratatui::style::{Color, Modifier, Style};
4use ratatui::text::{Line, Span};
5use ratatui::widgets::{Block, BorderType, Borders, Clear, Paragraph, Tabs, Wrap};
6use ratatui_textarea::TextArea;
7
8use super::app::{App, Focus, MemTab, Mode, OutTab, PromptKind, SPEEDS};
9use super::syntax::highlight;
10use crate::sfr::{self, IE, IP, PCON, PSW, SCON, TCON, TH0, TH1, TL0, TL1, TMOD};
11
12const ACCENT: Color = Color::Cyan;
13const CHANGED: Style = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD);
14const DIM: Style = Style::new().fg(Color::DarkGray);
15const LABEL: Style = Style::new().fg(Color::Gray);
16const ON: Style = Style::new()
17 .fg(Color::LightGreen)
18 .add_modifier(Modifier::BOLD);
19const PC_BG: Color = Color::Indexed(58);
20const CURSOR_BG: Color = Color::Indexed(236);
21
22pub fn style_editor(ed: &mut TextArea<'static>) {
23 ed.set_line_number_style(DIM);
24 ed.set_cursor_line_style(Style::new().bg(CURSOR_BG));
25 ed.set_tab_length(8);
26}
27
28fn panel(title: &str, focused: bool) -> Block<'_> {
29 Block::default()
30 .borders(Borders::ALL)
31 .border_type(BorderType::Rounded)
32 .border_style(if focused {
33 Style::new().fg(ACCENT)
34 } else {
35 DIM
36 })
37 .title(Span::styled(
38 format!(" {title} "),
39 if focused {
40 Style::new().fg(ACCENT).bold()
41 } else {
42 LABEL
43 },
44 ))
45}
46
47pub fn draw(f: &mut Frame, app: &mut App) {
48 let [header, body, footer] = Layout::vertical([
49 Constraint::Length(1),
50 Constraint::Min(5),
51 Constraint::Length(1),
52 ])
53 .areas(f.area());
54 draw_header(f, app, header);
55 match app.mode {
56 Mode::Edit => draw_edit(f, app, body),
57 Mode::Debug => draw_debug(f, app, body),
58 }
59 draw_footer(f, app, footer);
60 if app.show_help {
61 draw_help(f);
62 }
63}
64
65fn draw_header(f: &mut Frame, app: &App, area: Rect) {
66 let (mode, mode_style) = match app.mode {
67 Mode::Edit => (
68 " EDIT ",
69 Style::new().bg(Color::Blue).fg(Color::White).bold(),
70 ),
71 Mode::Debug => (
72 " DEBUG ",
73 Style::new().bg(Color::Magenta).fg(Color::White).bold(),
74 ),
75 };
76 let (state, state_style) = if app.running {
77 (
78 " RUNNING ",
79 Style::new().bg(Color::Green).fg(Color::Black).bold(),
80 )
81 } else if app.build.is_none() {
82 (
83 " NOT BUILT ",
84 Style::new().bg(Color::DarkGray).fg(Color::White),
85 )
86 } else {
87 (
88 " STOPPED ",
89 Style::new().bg(Color::Yellow).fg(Color::Black).bold(),
90 )
91 };
92 let mut spans = vec![
93 Span::styled(" bench51 ", Style::new().bg(ACCENT).fg(Color::Black).bold()),
94 Span::raw(" "),
95 Span::styled(mode, mode_style),
96 Span::styled(state, state_style),
97 Span::raw(format!(" {}", app.title())),
98 ];
99 if app.build.is_some() && app.stale() {
100 spans.push(Span::styled(
101 " (source changed — F7 to rebuild)",
102 Style::new().fg(Color::Yellow),
103 ));
104 }
105 spans.push(Span::styled(
106 format!(" speed {}/frame (+/-)", SPEEDS[app.speed]),
107 DIM,
108 ));
109 f.render_widget(Line::from(spans), area);
110}
111
112fn draw_footer(f: &mut Frame, app: &App, area: Rect) {
113 if let Some(p) = &app.prompt {
114 let label = match p.kind {
115 PromptKind::Open => "Open file (.asm or .hex): ",
116 PromptKind::SaveAs => "Save as: ",
117 PromptKind::Poke => match app.mem_tab {
118 MemTab::Iram => "Write IRAM — <addr> <value> (hex): ",
119 MemTab::Sfr => "Write SFR — <addr> <value> (hex): ",
120 MemTab::Xram => "Write XRAM — <addr> <value> (hex): ",
121 MemTab::Code => "Write CODE — <addr> <value> (hex): ",
122 },
123 };
124 let line = Line::from(vec![
125 Span::styled(label, Style::new().fg(ACCENT).bold()),
126 Span::raw(p.input.clone()),
127 Span::styled("█", Style::new().fg(ACCENT)),
128 Span::styled(" Enter confirm · Esc cancel", DIM),
129 ]);
130 f.render_widget(line, area);
131 return;
132 }
133 let keys = match app.mode {
134 Mode::Edit => "F7 build F5 run F11 step F9 bp F4 debug ^G $EDITOR F1 help",
135 Mode::Debug => {
136 "F5 run/pause F11 step F10 over F12 out F8 to cursor F9 bp F6 reset Tab focus F1 help"
137 }
138 };
139 let width = area.width as usize;
140 let status = format!(" {}", app.status);
141 let pad = width.saturating_sub(status.chars().count() + keys.len() + 1);
142 let line = Line::from(vec![
143 Span::styled(status, Style::new().fg(Color::White)),
144 Span::raw(" ".repeat(pad)),
145 Span::styled(keys, DIM),
146 ]);
147 f.render_widget(line, area);
148}
149
150// ---------------------------------------------------------------- Edit mode
151
152fn draw_edit(f: &mut Frame, app: &mut App, area: Rect) {
153 let [left, right] =
154 Layout::horizontal([Constraint::Percentage(58), Constraint::Percentage(42)]).areas(area);
155
156 let block = panel("Editor", true);
157 let inner = block.inner(left);
158 f.render_widget(block, left);
159 let [gutter, text] =
160 Layout::horizontal([Constraint::Length(2), Constraint::Min(1)]).areas(inner);
161 f.render_widget(&app.editor, text);
162
163 // Breakpoint / error markers aligned with the editor's scroll position.
164 let top = app.editor.scroll_offset().0 as usize;
165 let errors: Vec<usize> = app
166 .last_asm
167 .as_ref()
168 .filter(|(src, _)| *src == app.text())
169 .map(|(_, o)| o.diagnostics.iter().map(|d| d.line).collect())
170 .unwrap_or_default();
171 let marks: Vec<Line> = (top..top + gutter.height as usize)
172 .map(|l| {
173 if errors.contains(&l) {
174 Line::styled("✗", Style::new().fg(Color::Red).bold())
175 } else if app.breakpoints.contains(&l) {
176 Line::styled("●", Style::new().fg(Color::Red))
177 } else {
178 Line::raw("")
179 }
180 })
181 .collect();
182 f.render_widget(Paragraph::new(marks), gutter);
183
184 draw_output(f, app, right);
185}
186
187fn draw_output(f: &mut Frame, app: &mut App, area: Rect) {
188 let n_err = app
189 .last_asm
190 .as_ref()
191 .map_or(0, |(_, o)| o.diagnostics.len());
192 let block = panel("Output", false);
193 let inner = block.inner(area);
194 f.render_widget(block, area);
195 let [tabs_area, content] =
196 Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(inner);
197 let err_title = if n_err > 0 {
198 format!("Errors ({n_err})")
199 } else {
200 "Errors".to_string()
201 };
202 let selected = match app.out_tab {
203 OutTab::Listing => 0,
204 OutTab::Hex => 1,
205 OutTab::Errors => 2,
206 };
207 let tabs = Tabs::new(vec![
208 "Listing [Alt-1]".to_string(),
209 "HEX [Alt-2]".to_string(),
210 format!("{err_title} [Alt-3]"),
211 ])
212 .select(selected)
213 .style(DIM)
214 .highlight_style(Style::new().fg(ACCENT).bold().underlined());
215 f.render_widget(tabs, tabs_area);
216
217 let lines: Vec<Line> = match (app.out_tab, &app.last_asm) {
218 (_, None) => vec![Line::styled("Press F7 to assemble.", DIM)],
219 (OutTab::Listing, Some((src, out))) => out
220 .listing(src)
221 .lines()
222 .map(|l| {
223 if l.starts_with("***") {
224 Line::styled(l.to_string(), Style::new().fg(Color::Red).bold())
225 } else if l.len() > 25 && l.as_bytes()[0].is_ascii_hexdigit() {
226 // LOC OBJ LINE | source
227 let (head, src) = l.split_at(25);
228 let mut spans =
229 vec![Span::styled(head.to_string(), Style::new().fg(Color::Gray))];
230 spans.extend(highlight(src, Style::new()));
231 Line::from(spans)
232 } else if l.len() > 25 && l.starts_with(' ') {
233 let (head, src) = l.split_at(25);
234 let mut spans = vec![Span::styled(head.to_string(), DIM)];
235 spans.extend(highlight(src, Style::new()));
236 Line::from(spans)
237 } else {
238 Line::styled(l.to_string(), LABEL)
239 }
240 })
241 .collect(),
242 (OutTab::Hex, Some((_, out))) if out.ok() => {
243 out.intel_hex().lines().map(hex_line).collect()
244 }
245 (OutTab::Hex, Some(_)) => vec![Line::styled("Fix the errors to generate HEX output.", DIM)],
246 (OutTab::Errors, Some((_, out))) if out.ok() => {
247 vec![Line::styled("No errors.", Style::new().fg(Color::Green))]
248 }
249 (OutTab::Errors, Some((_, out))) => out
250 .diagnostics
251 .iter()
252 .map(|d| {
253 Line::from(vec![
254 Span::styled(
255 format!("line {:>4}:{:<3} ", d.line + 1, d.col + 1),
256 Style::new().fg(Color::Red),
257 ),
258 Span::raw(d.msg.clone()),
259 ])
260 })
261 .collect(),
262 };
263 app.out_scroll = app.out_scroll.min(lines.len().saturating_sub(1));
264 f.render_widget(
265 Paragraph::new(lines).scroll((app.out_scroll as u16, 0)),
266 content,
267 );
268}
269
270/// Colors the fields of an Intel HEX record.
271fn hex_line(l: &str) -> Line<'static> {
272 if l.len() < 11 {
273 return Line::raw(l.to_string());
274 }
275 let n = l.len();
276 Line::from(vec![
277 Span::styled(":", DIM),
278 Span::styled(l[1..3].to_string(), Style::new().fg(Color::Magenta)),
279 Span::styled(l[3..7].to_string(), Style::new().fg(Color::Yellow)),
280 Span::styled(l[7..9].to_string(), Style::new().fg(Color::Blue)),
281 Span::raw(l[9..n - 2].to_string()),
282 Span::styled(l[n - 2..].to_string(), DIM),
283 ])
284}
285
286// --------------------------------------------------------------- Debug mode
287
288fn draw_debug(f: &mut Frame, app: &mut App, area: Rect) {
289 let [left, right] =
290 Layout::horizontal([Constraint::Percentage(45), Constraint::Percentage(55)]).areas(area);
291 draw_source(f, app, left);
292
293 let [top, mid, bottom] = Layout::vertical([
294 Constraint::Length(10),
295 Constraint::Min(6),
296 Constraint::Length(8),
297 ])
298 .areas(right);
299 let [regs, periph] =
300 Layout::horizontal([Constraint::Length(30), Constraint::Min(20)]).areas(top);
301 let [ports, serial] =
302 Layout::horizontal([Constraint::Length(26), Constraint::Min(20)]).areas(bottom);
303 draw_registers(f, app, regs);
304 draw_peripherals(f, app, periph);
305 draw_memory(f, app, mid);
306 draw_ports(f, app, ports);
307 draw_serial(f, app, serial);
308}
309
310fn draw_source(f: &mut Frame, app: &mut App, area: Rect) {
311 let focused = app.focus == Focus::Source;
312 let block = panel("Source", focused);
313 let inner = block.inner(area);
314 f.render_widget(block, area);
315 let Some(build) = &app.build else {
316 f.render_widget(
317 Paragraph::new("Nothing assembled yet — press F7.").style(DIM),
318 inner,
319 );
320 return;
321 };
322
323 let h = inner.height as usize;
324 if app.src_cursor < app.src_scroll {
325 app.src_scroll = app.src_cursor;
326 } else if app.src_cursor >= app.src_scroll + h {
327 app.src_scroll = app.src_cursor + 1 - h;
328 }
329 let pc_line = build.addr_to_line.get(&app.sim.pc).copied();
330 let lines: Vec<Line> = build
331 .source
332 .iter()
333 .enumerate()
334 .skip(app.src_scroll)
335 .take(h)
336 .map(|(i, text)| {
337 let out = &build.out.lines[i];
338 let is_pc = pc_line == Some(i);
339 let bg = if is_pc {
340 Style::new().bg(PC_BG)
341 } else if i == app.src_cursor && focused {
342 Style::new().bg(CURSOR_BG)
343 } else {
344 Style::new()
345 };
346 let bp = if app.breakpoints.contains(&i) {
347 Span::styled("●", bg.fg(Color::Red))
348 } else {
349 Span::styled(" ", bg)
350 };
351 let arrow = if is_pc {
352 Span::styled("▶", bg.fg(Color::Yellow).bold())
353 } else {
354 Span::styled(" ", bg)
355 };
356 let addr = match (out.addr, out.bytes.is_empty()) {
357 (Some(a), false) => Span::styled(format!(" {a:04X} "), bg.fg(Color::Gray)),
358 _ => Span::styled(" ", bg),
359 };
360 let mut spans = vec![bp, arrow, addr];
361 spans.extend(highlight(text, bg));
362 let used: usize = spans.iter().map(|s| s.content.chars().count()).sum();
363 spans.push(Span::styled(
364 " ".repeat((inner.width as usize).saturating_sub(used)),
365 bg,
366 ));
367 Line::from(spans)
368 })
369 .collect();
370 f.render_widget(Paragraph::new(lines), inner);
371}
372
373fn hex2(v: u8, changed: bool) -> Span<'static> {
374 Span::styled(
375 format!("{v:02X}"),
376 if changed {
377 CHANGED
378 } else {
379 Style::new().fg(Color::White)
380 },
381 )
382}
383
384fn flags(byte: u8, names: [&'static str; 8]) -> Vec<Span<'static>> {
385 names
386 .iter()
387 .enumerate()
388 .filter(|(_, n)| !n.is_empty())
389 .flat_map(|(i, &n)| {
390 let on = byte & (0x80 >> i) != 0;
391 [Span::styled(n, if on { ON } else { DIM }), Span::raw(" ")]
392 })
393 .collect()
394}
395
396fn draw_registers(f: &mut Frame, app: &App, area: Rect) {
397 let s = &app.sim;
398 let block = panel("Registers", false);
399 let inner = block.inner(area);
400 f.render_widget(block, area);
401 let ch = |addr: u8| s.sfr(addr) != app.prev_sfr[(addr & 0x7F) as usize];
402 let reg = |n: u8| {
403 let a = s.reg_addr(n) as usize;
404 vec![
405 Span::styled(format!("R{n} "), LABEL),
406 hex2(s.iram[a], s.iram[a] != app.prev_iram[a]),
407 Span::raw(" "),
408 ]
409 };
410 let micros = s.cycles as f64 * 12.0 / app.clock_mhz;
411 let time = if micros >= 1_000_000.0 {
412 format!("{:.3} s", micros / 1e6)
413 } else if micros >= 1000.0 {
414 format!("{:.3} ms", micros / 1e3)
415 } else {
416 format!("{micros:.2} µs")
417 };
418 let lines = vec![
419 Line::from(vec![
420 Span::styled("PC ", LABEL),
421 Span::styled(
422 format!("{:04X}", s.pc),
423 Style::new().fg(Color::Yellow).bold(),
424 ),
425 Span::styled(" DPTR ", LABEL),
426 Span::styled(
427 format!("{:04X}", s.dptr()),
428 if ch(sfr::DPL) || ch(sfr::DPH) {
429 CHANGED
430 } else {
431 Style::new()
432 },
433 ),
434 ]),
435 Line::from(vec![
436 Span::styled("A ", LABEL),
437 hex2(s.acc(), ch(sfr::ACC)),
438 Span::styled(" B ", LABEL),
439 hex2(s.b(), ch(sfr::B)),
440 Span::styled(" SP ", LABEL),
441 hex2(s.sp(), ch(sfr::SP)),
442 ]),
443 Line::from([0, 1, 2, 3].into_iter().flat_map(reg).collect::<Vec<_>>()),
444 Line::from([4, 5, 6, 7].into_iter().flat_map(reg).collect::<Vec<_>>()),
445 Line::from(
446 [
447 Span::styled("PSW ", LABEL),
448 hex2(s.psw(), ch(PSW)),
449 Span::raw(" "),
450 ]
451 .into_iter()
452 .chain(flags(s.psw(), ["C", "AC", "F0", "", "", "OV", "", "P"]))
453 .collect::<Vec<_>>(),
454 ),
455 Line::from(vec![
456 Span::styled("Bank ", LABEL),
457 Span::raw(format!("{}", s.bank())),
458 Span::styled(" Cycles ", LABEL),
459 Span::raw(format!("{}", s.cycles)),
460 ]),
461 Line::from(vec![
462 Span::styled("Time ", LABEL),
463 Span::raw(time),
464 Span::styled(format!(" @{}MHz", app.clock_mhz), DIM),
465 ]),
466 ];
467 f.render_widget(Paragraph::new(lines), inner);
468}
469
470fn draw_peripherals(f: &mut Frame, app: &App, area: Rect) {
471 let s = &app.sim;
472 let block = panel("Peripherals", false);
473 let inner = block.inner(area);
474 f.render_widget(block, area);
475 let ch = |addr: u8| s.sfr(addr) != app.prev_sfr[(addr & 0x7F) as usize];
476 let sfr_val = |name: &'static str, addr: u8| {
477 vec![
478 Span::styled(name, LABEL),
479 hex2(s.sfr(addr), ch(addr)),
480 Span::raw(" "),
481 ]
482 };
483
484 let tmod = s.sfr(TMOD);
485 let tcon = s.sfr(TCON);
486 let timer = |n: u8| {
487 let ctl = (tmod >> (4 * n)) & 0x0F;
488 let (th, tl, tr) = if n == 0 {
489 (TH0, TL0, 0x10)
490 } else {
491 (TH1, TL1, 0x40)
492 };
493 let kind = if ctl & 4 != 0 { "ctr" } else { "tmr" };
494 let gate = if ctl & 8 != 0 { " gate" } else { "" };
495 vec![
496 Span::styled(format!("T{n} "), LABEL),
497 Span::styled(
498 format!("{:02X}{:02X}", s.sfr(th), s.sfr(tl)),
499 if ch(th) || ch(tl) {
500 CHANGED
501 } else {
502 Style::new()
503 },
504 ),
505 Span::styled(format!(" m{} {kind}{gate} ", ctl & 3), DIM),
506 if tcon & tr != 0 {
507 Span::styled("run", ON)
508 } else {
509 Span::styled("stop", DIM)
510 },
511 Span::raw(" "),
512 ]
513 };
514
515 let scon = s.sfr(SCON);
516 let irq = if s.in_service.is_empty() {
517 Span::styled("none", DIM)
518 } else {
519 Span::styled(
520 s.in_service
521 .iter()
522 .map(|l| format!("L{l}"))
523 .collect::<Vec<_>>()
524 .join(">"),
525 Style::new().fg(Color::Yellow),
526 )
527 };
528 let last = s.last_irq.map_or("-".to_string(), |v| {
529 format!("{v:04X}h ({})", vector_name(v))
530 });
531 let mut cpu_state = vec![];
532 if s.idle {
533 cpu_state.push(Span::styled(" IDLE", Style::new().fg(Color::Yellow).bold()));
534 }
535 if s.power_down {
536 cpu_state.push(Span::styled(
537 " POWER-DOWN",
538 Style::new().fg(Color::Red).bold(),
539 ));
540 }
541
542 let lines = vec![
543 Line::from([sfr_val("TMOD ", TMOD), timer(0), timer(1)].concat()),
544 Line::from(
545 [
546 sfr_val("TCON ", TCON),
547 flags(
548 tcon,
549 ["TF1", "TR1", "TF0", "TR0", "IE1", "IT1", "IE0", "IT0"],
550 ),
551 ]
552 .concat(),
553 ),
554 Line::from(
555 [
556 sfr_val("SCON ", SCON),
557 vec![Span::styled(format!("mode{} ", scon >> 6), DIM)],
558 flags(scon, ["", "", "SM2", "REN", "TB8", "RB8", "TI", "RI"]),
559 ]
560 .concat(),
561 ),
562 Line::from(vec![
563 Span::styled("SBUF ", LABEL),
564 Span::styled("rx ", DIM),
565 hex2(s.uart.sbuf_rx, false),
566 Span::styled(
567 if s.uart.transmitting() {
568 " tx busy"
569 } else {
570 " tx idle"
571 },
572 DIM,
573 ),
574 Span::styled(
575 format!(" {} byte(s) queued", s.uart_rx_pending().len()),
576 DIM,
577 ),
578 ]),
579 Line::from(
580 [
581 sfr_val("IE ", IE),
582 flags(s.sfr(IE), ["EA", "", "", "ES", "ET1", "EX1", "ET0", "EX0"]),
583 ]
584 .concat(),
585 ),
586 Line::from(
587 [
588 sfr_val("IP ", IP),
589 flags(s.sfr(IP), ["", "", "", "PS", "PT1", "PX1", "PT0", "PX0"]),
590 sfr_val(" PCON ", PCON),
591 flags(s.sfr(PCON), ["SMOD", "", "", "", "", "", "PD", "IDL"]),
592 ]
593 .concat(),
594 ),
595 Line::from(
596 [
597 vec![
598 Span::styled("IRQ ", LABEL),
599 Span::styled("active ", DIM),
600 irq,
601 Span::styled(" last ", DIM),
602 ],
603 vec![Span::raw(last)],
604 cpu_state,
605 ]
606 .concat(),
607 ),
608 ];
609 f.render_widget(Paragraph::new(lines), inner);
610}
611
612fn vector_name(v: u16) -> &'static str {
613 match v {
614 0x03 => "INT0",
615 0x0B => "T0",
616 0x13 => "INT1",
617 0x1B => "T1",
618 0x23 => "UART",
619 _ => "?",
620 }
621}
622
623fn draw_memory(f: &mut Frame, app: &mut App, area: Rect) {
624 let focused = app.focus == Focus::Memory;
625 let block = panel("Memory", focused);
626 let inner = block.inner(area);
627 f.render_widget(block, area);
628 let [tabs_area, content] =
629 Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(inner);
630 let selected = match app.mem_tab {
631 MemTab::Iram => 0,
632 MemTab::Sfr => 1,
633 MemTab::Xram => 2,
634 MemTab::Code => 3,
635 };
636 f.render_widget(
637 Tabs::new(vec!["IRAM [i]", "SFR [f]", "XRAM [d]", "CODE [c]"])
638 .select(selected)
639 .style(DIM)
640 .highlight_style(Style::new().fg(ACCENT).bold().underlined()),
641 tabs_area,
642 );
643
644 if app.mem_tab == MemTab::Sfr {
645 draw_sfr_table(f, app, content);
646 return;
647 }
648
649 let s = &app.sim;
650 let per_row: usize = if content.width >= 74 { 16 } else { 8 };
651 let total = match app.mem_tab {
652 MemTab::Iram => 256,
653 _ => 0x10000,
654 };
655 let rows = total / per_row;
656 let h = content.height as usize;
657 app.mem_scroll = app.mem_scroll.min(rows.saturating_sub(h));
658 let bps = app.bp_addrs();
659 let pc = s.pc as usize;
660 let pc_len = crate::isa::info(s.code[pc]).map_or(1, |i| i.size as usize);
661 let bank = s.reg_addr(0) as usize;
662
663 let lines: Vec<Line> = (app.mem_scroll..rows.min(app.mem_scroll + h))
664 .map(|row| {
665 let base = row * per_row;
666 let mut spans = vec![Span::styled(
667 if total > 256 {
668 format!("{base:04X} ")
669 } else {
670 format!("{base:02X} ")
671 },
672 LABEL,
673 )];
674 let mut ascii = String::new();
675 for i in 0..per_row {
676 let a = base + i;
677 let (v, style) = match app.mem_tab {
678 MemTab::Iram => {
679 let v = s.iram[a];
680 let st = if a == s.sp() as usize {
681 Style::new().fg(Color::Black).bg(Color::LightMagenta)
682 } else if v != app.prev_iram[a] {
683 CHANGED
684 } else if (bank..bank + 8).contains(&a) {
685 Style::new().fg(Color::LightBlue)
686 } else if v == 0 {
687 DIM
688 } else {
689 Style::new()
690 };
691 (v, st)
692 }
693 MemTab::Xram => {
694 let v = s.xram[a];
695 (v, if v == 0 { DIM } else { Style::new() })
696 }
697 _ => {
698 let v = s.code[a];
699 let st = if (pc..pc + pc_len).contains(&a) {
700 Style::new().fg(Color::Black).bg(Color::Yellow)
701 } else if bps.contains(&(a as u16)) {
702 Style::new().fg(Color::White).bg(Color::Red)
703 } else if v == 0 {
704 DIM
705 } else {
706 Style::new()
707 };
708 (v, st)
709 }
710 };
711 spans.push(Span::styled(format!("{v:02X}"), style));
712 spans.push(Span::raw(if i == 7 && per_row == 16 { " " } else { " " }));
713 ascii.push(if v.is_ascii_graphic() || v == b' ' {
714 v as char
715 } else {
716 '·'
717 });
718 }
719 spans.push(Span::styled(ascii, DIM));
720 Line::from(spans)
721 })
722 .collect();
723 f.render_widget(Paragraph::new(lines), content);
724}
725
726fn draw_sfr_table(f: &mut Frame, app: &App, area: Rect) {
727 let s = &app.sim;
728 let cols = if area.width >= 64 { 3 } else { 2 };
729 let per_col = sfr::SFRS.len().div_ceil(cols);
730 let lines: Vec<Line> = (0..per_col)
731 .map(|r| {
732 let mut spans = Vec::new();
733 for c in 0..cols {
734 if let Some(&(name, addr)) = sfr::SFRS.get(c * per_col + r) {
735 let v = s.read_direct(addr, true);
736 let changed = s.sfr(addr) != app.prev_sfr[(addr & 0x7F) as usize];
737 spans.push(Span::styled(format!("{name:<5}"), LABEL));
738 spans.push(Span::styled(format!("{addr:02X} "), DIM));
739 spans.push(hex2(v, changed));
740 spans.push(Span::styled(
741 format!(" {v:08b} "),
742 if changed { CHANGED } else { DIM },
743 ));
744 }
745 }
746 Line::from(spans)
747 })
748 .collect();
749 f.render_widget(Paragraph::new(lines), area);
750}
751
752fn draw_ports(f: &mut Frame, app: &App, area: Rect) {
753 let focused = app.focus == Focus::Ports;
754 let block = panel("Ports", focused);
755 let inner = block.inner(area);
756 f.render_widget(block, area);
757 let s = &app.sim;
758 let mut lines: Vec<Line> = (0..4)
759 .map(|p| {
760 let latch = s.sfr(sfr::P0 + 0x10 * p as u8);
761 let pins = s.port_pins(p);
762 let mut spans = vec![Span::styled(format!("P{p} "), LABEL)];
763 for bit in (0..8u8).rev() {
764 let m = 1 << bit;
765 let (sym, mut st) = if pins & m != 0 {
766 ("●", Style::new().fg(Color::LightGreen))
767 } else if s.pins[p] & m == 0 {
768 // Pulled low from outside (e.g. a pressed button).
769 ("○", Style::new().fg(Color::LightRed))
770 } else {
771 ("○", Style::new().fg(Color::DarkGray))
772 };
773 if focused && app.port_sel == (p, bit) {
774 st = st.add_modifier(Modifier::REVERSED);
775 }
776 spans.push(Span::styled(sym, st));
777 spans.push(Span::raw(if bit == 4 { " " } else { " " }));
778 }
779 spans.push(Span::styled(format!("{latch:02X}"), DIM));
780 Line::from(spans)
781 })
782 .collect();
783 if focused {
784 lines.push(Line::styled("←→↑↓ select · space: pull pin low", DIM));
785 } else {
786 lines.push(Line::styled("●=1 ○=0 red=pulled low", DIM));
787 }
788 f.render_widget(Paragraph::new(lines), inner);
789}
790
791fn draw_serial(f: &mut Frame, app: &App, area: Rect) {
792 let focused = app.focus == Focus::Serial;
793 let block = panel("Serial (UART)", focused);
794 let inner = block.inner(area);
795 f.render_widget(block, area);
796 let [out_area, in_area] =
797 Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(inner);
798
799 let mut text = String::new();
800 for &b in &app.sim.uart.tx_log {
801 match b {
802 b'\n' => text.push('\n'),
803 b'\r' => {}
804 0x20..=0x7E => text.push(b as char),
805 _ => text.push('·'),
806 }
807 }
808 let lines: Vec<&str> = text.split('\n').collect();
809 let h = out_area.height as usize;
810 let shown: Vec<Line> = lines[lines.len().saturating_sub(h)..]
811 .iter()
812 .map(|l| Line::raw(l.to_string()))
813 .collect();
814 f.render_widget(Paragraph::new(shown).wrap(Wrap { trim: false }), out_area);
815
816 let input = if focused {
817 Line::from(vec![
818 Span::styled("> ", Style::new().fg(ACCENT)),
819 Span::raw(app.serial_input.clone()),
820 Span::styled("█", Style::new().fg(ACCENT)),
821 ])
822 } else {
823 Line::styled("Tab here to type; Enter sends text + CR", DIM)
824 };
825 f.render_widget(input, in_area);
826}
827
828fn draw_help(f: &mut Frame) {
829 let area = f.area();
830 let w = 72.min(area.width);
831 let h = 31.min(area.height);
832 let rect = Rect::new((area.width - w) / 2, (area.height - h) / 2, w, h);
833 f.render_widget(Clear, rect);
834 let k = |key: &str, desc: &str| {
835 Line::from(vec![
836 Span::styled(format!(" {key:<16}"), Style::new().fg(ACCENT).bold()),
837 Span::raw(desc.to_string()),
838 ])
839 };
840 let head = |t: &str| Line::styled(t.to_string(), Style::new().fg(Color::Yellow).bold());
841 let lines = vec![
842 head("Global"),
843 k("F1", "this help"),
844 k(
845 "F2 / Ctrl-S",
846 "save (writes .asm; F7 also writes .lst + .hex)",
847 ),
848 k("F3 / Ctrl-O", "open .asm, or .hex (disassembled)"),
849 k("Ctrl-G", "edit in $EDITOR; reloads + reassembles on exit"),
850 k("F4", "switch Edit ⇄ Debug"),
851 k("F7", "assemble (reset + load)"),
852 k("F5", "run / pause (assembles if needed)"),
853 k("F11 F10 F12", "step into · step over · step out"),
854 k("F8", "run to cursor line"),
855 k("F9", "toggle breakpoint on cursor line"),
856 k("F6", "reset CPU"),
857 k("Ctrl-Q", "quit"),
858 head("Edit mode"),
859 k("Alt-1/2/3", "Listing / HEX / Errors tab"),
860 k("Alt-↑↓ PgUp/Dn", "scroll output"),
861 k("Ctrl-U / Ctrl-R", "undo / redo (Emacs-style editing keys)"),
862 head("Debug mode"),
863 k("Tab / Shift-Tab", "focus Source → Memory → Ports → Serial"),
864 k(
865 "r s n o g b x",
866 "run · step · next · out · to cursor · bp · reset",
867 ),
868 k("i f d c", "memory: IRAM · SFR · XRAM · CODE"),
869 k("p", "poke a byte into the shown memory"),
870 k("+ / -", "simulation speed"),
871 k(".", "jump source cursor to PC"),
872 k("Esc", "pause / leave serial / back to editor"),
873 Line::raw(""),
874 Line::styled(" press any key to close", DIM),
875 ];
876 f.render_widget(
877 Paragraph::new(lines)
878 .block(panel("Help", true))
879 .style(Style::new().bg(Color::Black)),
880 rect,
881 );
882}