Krolyxon
Commit 815d036

Branches

Tags

Refs →

bench51srcuidraw.rs

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