| 1 | use ratatui::Frame; |
| 2 | use ratatui::layout::{Constraint, Layout, Rect}; |
| 3 | use ratatui::style::{Color, Modifier, Style}; |
| 4 | use ratatui::text::{Line, Span}; |
| 5 | use ratatui::widgets::{Block, BorderType, Borders, Clear, Paragraph, Tabs, Wrap}; |
| 6 | use ratatui_textarea::TextArea; |
| 7 | |
| 8 | use super::app::{App, Focus, MemTab, Mode, OutTab, PromptKind}; |
| 9 | use super::syntax::highlight; |
| 10 | use bench51_core::session::{SPEEDS, vector_name}; |
| 11 | use bench51_core::sfr::{self, IE, IP, PCON, PSW, SCON, TCON, TH0, TH1, TL0, TL1, TMOD}; |
| 12 | |
| 13 | const ACCENT: Color = Color::Cyan; |
| 14 | const CHANGED: Style = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD); |
| 15 | const DIM: Style = Style::new().fg(Color::DarkGray); |
| 16 | const LABEL: Style = Style::new().fg(Color::Gray); |
| 17 | const ON: Style = Style::new() |
| 18 | .fg(Color::LightGreen) |
| 19 | .add_modifier(Modifier::BOLD); |
| 20 | const PC_BG: Color = Color::Indexed(58); |
| 21 | const CURSOR_BG: Color = Color::Indexed(236); |
| 22 | |
| 23 | pub 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 | |
| 29 | fn 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 | |
| 48 | pub 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 | |
| 66 | fn 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 | |
| 113 | fn 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 | |
| 153 | fn 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 | |
| 188 | fn 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. |
| 273 | fn 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 | |
| 290 | fn 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 | |
| 312 | fn 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 | |
| 375 | fn 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 | |
| 386 | fn 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 | |
| 398 | fn 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 | |
| 465 | fn 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 | |
| 607 | fn 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 | |
| 710 | fn 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 | |
| 736 | fn 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 | |
| 775 | fn 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 | |
| 812 | fn 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 | } |