| 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, SPEEDS}; |
| 9 | use super::syntax::highlight; |
| 10 | use crate::sfr::{self, IE, IP, PCON, PSW, SCON, TCON, TH0, TH1, TL0, TL1, TMOD}; |
| 11 | |
| 12 | const ACCENT: Color = Color::Cyan; |
| 13 | const CHANGED: Style = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD); |
| 14 | const DIM: Style = Style::new().fg(Color::DarkGray); |
| 15 | const LABEL: Style = Style::new().fg(Color::Gray); |
| 16 | const ON: Style = Style::new() |
| 17 | .fg(Color::LightGreen) |
| 18 | .add_modifier(Modifier::BOLD); |
| 19 | const PC_BG: Color = Color::Indexed(58); |
| 20 | const CURSOR_BG: Color = Color::Indexed(236); |
| 21 | |
| 22 | pub 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 | |
| 28 | fn 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 | |
| 47 | pub 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 | |
| 65 | fn 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(" i8051 ", 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 | |
| 112 | fn 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 | |
| 152 | fn 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 | |
| 187 | fn 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. |
| 271 | fn 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 | |
| 288 | fn 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 | |
| 310 | fn 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 | |
| 373 | fn 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 | |
| 384 | fn 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 | |
| 396 | fn 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 | |
| 470 | fn 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 | |
| 612 | fn 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 | |
| 623 | fn 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 | |
| 726 | fn 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 | |
| 752 | fn 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 | |
| 791 | fn 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 | |
| 828 | fn 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 | } |