i8051_debug_tui/
lib.rs

1use std::borrow::Cow;
2use std::cell::Cell;
3use std::collections::{BTreeSet, VecDeque};
4use std::io;
5use std::sync::{Arc, Mutex};
6
7use i8051::{ControlFlow, Cpu, CpuContext, Flag, Register};
8use ratatui::text::Text;
9use ratatui::{
10    Frame, Terminal,
11    backend::CrosstermBackend,
12    crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers},
13    layout::{Constraint, Direction, Layout, Rect},
14    style::{Color, Modifier, Style},
15    text::{Line, Span},
16    widgets::{Paragraph, Wrap},
17};
18use tracing::Level;
19use tracing_subscriber::Layer;
20
21// Re-export crossterm from ratatui so users get the matching version
22pub use ratatui::crossterm;
23
24/// Inner state for the tracing collector
25#[derive(Clone)]
26struct TracingCollectorInner {
27    max_events: usize,
28    events: Arc<Mutex<VecDeque<(Level, String)>>>,
29}
30
31/// A tracing collector that collects events and displays them in a TUI.
32#[derive(Clone)]
33pub struct TracingCollector {
34    inner: TracingCollectorInner,
35}
36
37impl TracingCollector {
38    pub fn new(max_events: usize) -> Self {
39        Self {
40            inner: TracingCollectorInner {
41                max_events,
42                events: Arc::new(Mutex::new(VecDeque::with_capacity(max_events))),
43            },
44        }
45    }
46
47    pub fn with_last_n(&self, n: usize, mut f: impl FnMut(Level, &str)) {
48        let events = self.inner.events.lock().unwrap();
49        for event in events.iter().rev().take(n) {
50            f(event.0, &event.1);
51        }
52    }
53
54    /// Clear all collected events
55    pub fn clear(&self) {
56        self.inner.events.lock().unwrap().clear();
57    }
58
59    fn add_event(&self, level: Level, message: String) {
60        let mut events = self.inner.events.lock().unwrap();
61        if events.len() >= self.inner.max_events {
62            events.pop_front();
63        }
64        events.push_back((level, message));
65    }
66}
67
68impl<S> Layer<S> for TracingCollector
69where
70    S: tracing::Subscriber,
71{
72    fn on_event(
73        &self,
74        event: &tracing::Event<'_>,
75        _ctx: tracing_subscriber::layer::Context<'_, S>,
76    ) {
77        struct Filter(bool);
78        impl tracing::field::Visit for Filter {
79            fn record_debug(&mut self, _: &tracing::field::Field, _: &dyn std::fmt::Debug) {}
80            fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
81                if field.name() == "log.module_path" && value.starts_with("mio::") {
82                    self.0 = true;
83                }
84            }
85        }
86
87        let mut filter = Filter(false);
88        event.record(&mut filter);
89        if filter.0 {
90            return;
91        }
92
93        // Format the event into a string
94        let mut visitor = EventVisitor::default();
95        event.record(&mut visitor);
96
97        let level = event.metadata().level();
98        let message = visitor.message;
99
100        self.add_event(*level, message);
101    }
102}
103
104/// Visitor for extracting message from tracing events
105#[derive(Default)]
106struct EventVisitor {
107    message: String,
108}
109
110impl tracing::field::Visit for EventVisitor {
111    fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
112        if field.name() == "message" {
113            self.message = format!("{:?}", value);
114            // Remove the surrounding quotes that Debug adds
115            if self.message.starts_with('"') && self.message.ends_with('"') {
116                self.message = self.message[1..self.message.len() - 1].to_string();
117            }
118        } else if self.message.is_empty() {
119            // If no message field, use the first field we see
120            self.message = format!("{} = {:?}", field.name(), value);
121        } else {
122            // Append additional fields
123            self.message
124                .push_str(&format!(", {} = {:?}", field.name(), value));
125        }
126    }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub enum DebuggerState {
131    /// The CPU is paused.
132    Paused,
133    /// The CPU is running.
134    Running,
135    /// The debugger is quitting.
136    Quit,
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140enum DebuggerFocus {
141    Code,
142    Reg(Register),
143    Output,
144}
145
146impl DebuggerFocus {
147    const ORDER: [DebuggerFocus; 15] = [
148        DebuggerFocus::Code,
149        DebuggerFocus::Reg(Register::PC),
150        DebuggerFocus::Reg(Register::SP),
151        DebuggerFocus::Reg(Register::R(0)),
152        DebuggerFocus::Reg(Register::R(1)),
153        DebuggerFocus::Reg(Register::R(2)),
154        DebuggerFocus::Reg(Register::R(3)),
155        DebuggerFocus::Reg(Register::R(4)),
156        DebuggerFocus::Reg(Register::R(5)),
157        DebuggerFocus::Reg(Register::R(6)),
158        DebuggerFocus::Reg(Register::R(7)),
159        DebuggerFocus::Reg(Register::A),
160        DebuggerFocus::Reg(Register::B),
161        DebuggerFocus::Reg(Register::DPTR),
162        DebuggerFocus::Output,
163    ];
164
165    pub fn next(self) -> Self {
166        let index = Self::ORDER.iter().position(|r| r == &self).unwrap_or(0);
167        Self::ORDER[(index + 1) % Self::ORDER.len()]
168    }
169
170    pub fn prev(self) -> Self {
171        let index = Self::ORDER.iter().position(|r| r == &self).unwrap_or(0);
172        if index == 0 {
173            Self::ORDER[Self::ORDER.len() - 1]
174        } else {
175            Self::ORDER[index - 1]
176        }
177    }
178
179    /// Returns zero if not editable, otherwise the length.
180    #[allow(dead_code)]
181    pub fn edit_length(self) -> usize {
182        use DebuggerFocus::*;
183        match self {
184            Code | Output => 0,
185            Reg(Register::PC) | Reg(Register::DPTR) => 4,
186            Reg(Register::SP) | Reg(Register::A) | Reg(Register::B) | Reg(Register::R(_)) => 2,
187            _ => 0,
188        }
189    }
190}
191
192struct DebuggerInternalState {
193    /// The current TUI widget focus of the debugger.
194    focus: DebuggerFocus,
195    /// Current debugger state
196    state: DebuggerState,
197    /// The edit buffer for in-place register editing
198    edit_buffer: String,
199    /// Whether we're currently editing (if true, edit_buffer is shown in place of the focused register)
200    is_editing: bool,
201    /// The current code window state
202    code_window: CodeWindowState,
203}
204
205struct CodeWindowState {
206    /// The first address to display in the code window
207    start: Cell<u32>,
208    /// The last height of the code window
209    last_height: Cell<usize>,
210    /// The focus address to display in the code window, or None to use the PC
211    focus: Option<u32>,
212}
213
214/// Configuration for the debugger UI
215pub struct DebuggerConfig {
216    pub step_key_label: Cow<'static, str>,
217    pub run_key_label: Cow<'static, str>,
218    pub toggle_breakpoint_key_label: Cow<'static, str>,
219}
220
221impl Default for DebuggerConfig {
222    fn default() -> Self {
223        Self {
224            step_key_label: Cow::Borrowed("s"),
225            run_key_label: Cow::Borrowed("r"),
226            toggle_breakpoint_key_label: Cow::Borrowed("b"),
227        }
228    }
229}
230
231/// TUI Debugger for the i8051 emulator
232pub struct Debugger {
233    config: DebuggerConfig,
234    breakpoints: BTreeSet<u32>,
235    terminal: Terminal<CrosstermBackend<io::Stdout>>,
236    state: DebuggerInternalState,
237    tracing_collector: TracingCollector,
238}
239
240impl Debugger {
241    /// Create a new debugger with the given configuration
242    pub fn new(config: DebuggerConfig) -> io::Result<Self> {
243        let stdout = io::stdout();
244        let backend = CrosstermBackend::new(stdout);
245        let terminal = Terminal::new(backend)?;
246
247        Ok(Self {
248            config,
249            breakpoints: BTreeSet::new(),
250            terminal,
251            state: DebuggerInternalState {
252                focus: DebuggerFocus::Code,
253                state: DebuggerState::Paused,
254                edit_buffer: String::new(),
255                is_editing: false,
256                code_window: CodeWindowState {
257                    start: Cell::new(0),
258                    last_height: Cell::new(10),
259                    focus: None,
260                },
261            },
262            tracing_collector: TracingCollector::new(1000),
263        })
264    }
265
266    /// Get a clone of the tracing collector that can be used as a Layer
267    pub fn tracing_collector(&self) -> TracingCollector {
268        self.tracing_collector.clone()
269    }
270
271    /// Get a reference to the breakpoints set
272    pub fn breakpoints(&self) -> &BTreeSet<u32> {
273        &self.breakpoints
274    }
275
276    /// Get a mutable reference to the breakpoints set
277    pub fn breakpoints_mut(&mut self) -> &mut BTreeSet<u32> {
278        &mut self.breakpoints
279    }
280
281    /// Get the current debugger state
282    pub fn debugger_state(&self) -> DebuggerState {
283        self.state.state
284    }
285
286    /// Pause the debugger (set to paused state)
287    pub fn pause(&mut self) {
288        self.state.state = DebuggerState::Paused;
289        self.state.code_window.focus = None;
290    }
291
292    /// Toggle a breakpoint at the given address
293    pub fn toggle_breakpoint(&mut self, addr: u32) {
294        if self.breakpoints.contains(&addr) {
295            self.breakpoints.remove(&addr);
296        } else {
297            self.breakpoints.insert(addr);
298        }
299    }
300
301    /// Handle a crossterm event and update internal state
302    /// Returns true if we should step the CPU, false otherwise
303    pub fn handle_event(&mut self, event: Event, cpu: &mut Cpu, ctx: &mut impl CpuContext) -> bool {
304        // Handle editing mode
305        if self.state.is_editing {
306            match event {
307                Event::Key(KeyEvent {
308                    code: KeyCode::Esc, ..
309                }) => {
310                    // Cancel editing
311                    self.state.is_editing = false;
312                    self.state.edit_buffer.clear();
313                    return false;
314                }
315                Event::Key(KeyEvent {
316                    code: KeyCode::Enter,
317                    ..
318                }) => {
319                    // Apply the edit
320                    if let DebuggerFocus::Reg(register) = self.state.focus {
321                        if let Ok(value) = u32::from_str_radix(&self.state.edit_buffer, 16) {
322                            self.apply_register_edit(cpu, ctx, register, value);
323                        }
324                    }
325                    self.state.is_editing = false;
326                    self.state.edit_buffer.clear();
327                    return false;
328                }
329                Event::Key(KeyEvent {
330                    code: KeyCode::Backspace,
331                    ..
332                }) => {
333                    self.state.edit_buffer.pop();
334                    return false;
335                }
336                Event::Key(KeyEvent {
337                    code: KeyCode::Char(c),
338                    ..
339                }) if c.is_ascii_hexdigit() => {
340                    // Get max length for this register
341                    let max_len = match self.state.focus {
342                        DebuggerFocus::Reg(Register::PC) | DebuggerFocus::Reg(Register::DPTR) => 4,
343                        _ => 2,
344                    };
345                    if self.state.edit_buffer.len() < max_len {
346                        self.state.edit_buffer.push(c.to_ascii_uppercase());
347                    }
348                    return false;
349                }
350                _ => return false,
351            }
352        }
353
354        // Handle normal mode
355        match event {
356            Event::Key(KeyEvent {
357                code: KeyCode::Char('c'),
358                modifiers: KeyModifiers::CONTROL,
359                ..
360            }) => {
361                if self.state.state == DebuggerState::Running {
362                    self.state.state = DebuggerState::Paused;
363                }
364                false
365            }
366            Event::Key(KeyEvent {
367                code: KeyCode::Tab,
368                modifiers,
369                ..
370            }) => {
371                if modifiers.contains(KeyModifiers::SHIFT) {
372                    self.state.focus = self.state.focus.prev();
373                } else {
374                    self.state.focus = self.state.focus.next();
375                }
376                false
377            }
378            Event::Key(KeyEvent {
379                code: KeyCode::BackTab,
380                ..
381            }) => {
382                // BackTab is also Shift+Tab on some terminals
383                self.state.focus = self.state.focus.prev();
384                false
385            }
386            Event::Key(KeyEvent {
387                code: KeyCode::Char('q'),
388                ..
389            }) => {
390                self.state.state = DebuggerState::Quit;
391                true
392            }
393            Event::Key(KeyEvent {
394                code: KeyCode::Char('s'),
395                ..
396            }) if self.state.focus == DebuggerFocus::Code => {
397                // Step when code is focused, removing any custom address selection
398                self.state.code_window.focus = None;
399                true
400            }
401            Event::Key(KeyEvent {
402                code: KeyCode::Char('r'),
403                ..
404            }) if self.state.focus == DebuggerFocus::Code => {
405                // Toggle run/pause
406                self.state.state = match self.state.state {
407                    DebuggerState::Paused => DebuggerState::Running,
408                    DebuggerState::Running => DebuggerState::Paused,
409                    DebuggerState::Quit => DebuggerState::Quit,
410                };
411                false
412            }
413            Event::Key(KeyEvent {
414                code: KeyCode::Char('b'),
415                ..
416            }) if self.state.focus == DebuggerFocus::Code => {
417                // Toggle breakpoint
418                let addr = self.state.code_window.focus.unwrap_or(cpu.pc_ext(ctx));
419                self.toggle_breakpoint(addr);
420                false
421            }
422            Event::Key(KeyEvent {
423                code: KeyCode::Up, ..
424            }) if self.state.focus == DebuggerFocus::Code => {
425                // Scroll code window up (decrement start address)
426                let current = self.state.code_window.focus.unwrap_or(cpu.pc_ext(ctx));
427
428                // If we're at the top of the window, jump back 10 instructions and
429                // try to resynchronize
430                let mut start = self.state.code_window.start.get();
431                if start == current {
432                    start = start.saturating_sub(10);
433                }
434
435                // Render instructions and see if we can go back further
436                let instructions = cpu.decode_range(ctx, start, current, 100);
437                if let Some(current) = instructions
438                    .iter()
439                    .position(|instruction| instruction.pc() == current)
440                {
441                    self.state.code_window.start.set(instructions[0].pc());
442                    self.state.code_window.focus =
443                        Some(instructions[current.saturating_sub(1)].pc());
444                } else {
445                    // We failed to resynchronize, so just jump back 1 byte
446                    self.state.code_window.start.set(start);
447                    self.state.code_window.focus = Some(current.saturating_sub(1));
448                }
449                false
450            }
451            Event::Key(KeyEvent {
452                code: KeyCode::Down,
453                ..
454            }) if self.state.focus == DebuggerFocus::Code => {
455                // Scroll code window down (increment start address)
456                let current = self.state.code_window.focus.unwrap_or(cpu.pc_ext(ctx));
457                let height = self.state.code_window.last_height.get();
458                let instructions =
459                    cpu.decode_range(ctx, self.state.code_window.start.get(), current, 100);
460                let len = cpu.decode(ctx, current).len();
461                if let Some(current) = instructions
462                    .iter()
463                    .position(|instruction| instruction.pc() == current)
464                {
465                    let scroll = height.saturating_sub(current);
466                    if scroll <= 2 {
467                        self.state.code_window.start.set(instructions[2].pc());
468                    } else {
469                        self.state.code_window.start.set(instructions[0].pc());
470                    }
471                }
472                self.state.code_window.focus = Some(current.saturating_add(len as u32));
473
474                false
475            }
476            Event::Key(KeyEvent {
477                code: KeyCode::Char('c'),
478                ..
479            }) if self.state.focus == DebuggerFocus::Output => {
480                // Clear trace output
481                self.tracing_collector.clear();
482                false
483            }
484            Event::Key(KeyEvent {
485                code: KeyCode::Char(c),
486                ..
487            }) if matches!(self.state.focus, DebuggerFocus::Reg(_)) && c.is_ascii_hexdigit() => {
488                // Start editing the currently focused register
489                self.state.is_editing = true;
490                self.state.edit_buffer.clear();
491                self.state.edit_buffer.push(c.to_ascii_uppercase());
492                false
493            }
494            _ => false,
495        }
496    }
497
498    fn apply_register_edit(
499        &mut self,
500        cpu: &mut Cpu,
501        _ctx: &mut impl CpuContext,
502        register: Register,
503        value: u32,
504    ) {
505        cpu.register_set(register, value as _);
506    }
507
508    /// Render the debugger UI
509    pub fn render(&mut self, cpu: &Cpu, ctx: &mut impl CpuContext) -> io::Result<()> {
510        let config = &self.config;
511        let breakpoints = &self.breakpoints;
512        let focus = self.state.focus;
513        let state = self.state.state;
514        let is_editing = self.state.is_editing;
515        let edit_buffer = &self.state.edit_buffer;
516        let code_window = &self.state.code_window;
517        let trace_events = &self.tracing_collector;
518
519        self.terminal.draw(|f| {
520            render_frame(
521                f,
522                cpu,
523                ctx,
524                config,
525                breakpoints,
526                focus,
527                state,
528                is_editing,
529                edit_buffer,
530                &code_window,
531                &trace_events,
532            );
533        })?;
534        Ok(())
535    }
536}
537
538fn render_frame(
539    f: &mut Frame,
540    cpu: &Cpu,
541    ctx: &mut impl CpuContext,
542    config: &DebuggerConfig,
543    breakpoints: &BTreeSet<u32>,
544    focus: DebuggerFocus,
545    state: DebuggerState,
546    is_editing: bool,
547    edit_buffer: &str,
548    code_window: &CodeWindowState,
549    trace_events: &TracingCollector,
550) {
551    // Create main layout with status bar at bottom
552    let main_layout = Layout::default()
553        .direction(Direction::Vertical)
554        .constraints([
555            Constraint::Min(0),    // Main content area
556            Constraint::Length(1), // Status bar
557        ])
558        .split(f.area());
559
560    let content_area = main_layout[0];
561    let status_area = main_layout[1];
562
563    // Create the main content layout with a vertical separator
564    let main_chunks = Layout::default()
565        .direction(Direction::Horizontal)
566        .constraints([
567            Constraint::Percentage(50),
568            Constraint::Length(1), // Vertical separator
569            Constraint::Percentage(50),
570        ])
571        .split(content_area);
572
573    let right_chunks = Layout::default()
574        .direction(Direction::Vertical)
575        .constraints([Constraint::Min(10), Constraint::Percentage(50)])
576        .split(main_chunks[2]);
577
578    // Render the three panes
579    if state == DebuggerState::Paused {
580        render_disassembly(f, main_chunks[0], cpu, ctx, breakpoints, focus, code_window);
581    }
582
583    // Render vertical separator
584    let separator_lines: Vec<Line> = (0..main_chunks[1].height)
585        .map(|_| Line::from("│"))
586        .collect();
587    let separator = Paragraph::new(separator_lines).style(Style::default().fg(Color::DarkGray));
588    f.render_widget(separator, main_chunks[1]);
589
590    render_registers(f, right_chunks[0], cpu, focus, is_editing, edit_buffer);
591    render_output(f, right_chunks[1], trace_events, focus);
592
593    // Render status bar at the bottom
594    let status_text = format!(
595        " {} │ {}: Step │ {}: Run │ {}: Breakpoint │ Tab/Shift+Tab: Switch │ q: Quit ",
596        if state == DebuggerState::Running {
597            "RUNNING"
598        } else {
599            "PAUSED "
600        },
601        config.step_key_label,
602        config.run_key_label,
603        config.toggle_breakpoint_key_label,
604    );
605
606    let status_style = if state == DebuggerState::Running {
607        Style::default()
608            .bg(Color::Green)
609            .fg(Color::Black)
610            .add_modifier(Modifier::BOLD)
611    } else {
612        Style::default()
613            .bg(Color::Blue)
614            .fg(Color::White)
615            .add_modifier(Modifier::BOLD)
616    };
617
618    let status_bar = Paragraph::new(Line::from(Span::styled(status_text, status_style)));
619    f.render_widget(status_bar, status_area);
620}
621
622fn render_disassembly(
623    f: &mut Frame,
624    area: Rect,
625    cpu: &Cpu,
626    ctx: &mut impl CpuContext,
627    breakpoints: &BTreeSet<u32>,
628    focus: DebuggerFocus,
629    code_window: &CodeWindowState,
630) {
631    let pc = cpu.pc_ext(ctx);
632    let mut lines = Vec::new();
633    let code_window_focus = matches!(focus, DebuggerFocus::Code);
634
635    // Calculate how many instructions we can fit in the available area
636    let available_height = area.height as usize;
637    let max_lines = available_height.min(100);
638    code_window.last_height.set(max_lines);
639
640    // Decode the instructions for the code window, caching the start value if we need to.
641    let focus_addr = code_window.focus.unwrap_or(pc);
642    let mut instructions = cpu.decode_range(ctx, code_window.start.get(), focus_addr, max_lines);
643    if !instructions
644        .iter()
645        .any(|instruction| instruction.pc() == focus_addr)
646    {
647        instructions = cpu.decode_range(ctx, focus_addr.saturating_sub(10), focus_addr, max_lines);
648    }
649    code_window.start.set(instructions.first().unwrap().pc());
650
651    fn control_flow_addr(
652        cpu: &Cpu,
653        ctx: &impl CpuContext,
654        control_flow: ControlFlow,
655    ) -> Option<(&'static str, u32)> {
656        match control_flow {
657            ControlFlow::Call(_, pc) => Some((" ↦ ", cpu.pc_ext_addr(ctx, pc))),
658            ControlFlow::Choice(_, pc) => Some((" ↔ ", cpu.pc_ext_addr(ctx, pc))),
659            _ => None,
660        }
661    }
662
663    let control_flow = instructions
664        .iter()
665        .find(|instruction| instruction.pc() == focus_addr)
666        .map(|instruction| instruction.control_flow())
667        .and_then(|control_flow| control_flow_addr(cpu, ctx, control_flow));
668    let control_flow_pc = instructions
669        .iter()
670        .find(|instruction| instruction.pc() == pc)
671        .map(|instruction| instruction.control_flow())
672        .and_then(|control_flow| control_flow_addr(cpu, ctx, control_flow));
673
674    for instruction in instructions {
675        let bytes_str = instruction
676            .bytes()
677            .iter()
678            .map(|b| format!("{:02X}", b))
679            .collect::<Vec<_>>()
680            .join(" ");
681
682        let current_pc = instruction.pc();
683        let has_breakpoint = breakpoints.contains(&current_pc);
684        let is_current = current_pc == pc;
685        let is_focused = current_pc == focus_addr;
686
687        let bp_marker = if has_breakpoint { "●" } else { " " };
688        let pc_marker = if is_current { ">" } else { " " };
689
690        let control_flow_marker = control_flow.and_then(|(marker, addr)| {
691            if addr == current_pc {
692                Some(marker)
693            } else {
694                None
695            }
696        });
697        let control_flow_marker_pc = control_flow_pc.and_then(|(marker, addr)| {
698            if addr == current_pc {
699                Some(marker)
700            } else {
701                None
702            }
703        });
704
705        let line_text = format!(
706            "{}{} {:04X}: {:12} {} ",
707            bp_marker,
708            pc_marker,
709            current_pc,
710            bytes_str,
711            instruction.decode(),
712        );
713
714        let style = if is_current {
715            Style::default()
716                .fg(Color::Yellow)
717                .add_modifier(Modifier::BOLD)
718        } else if has_breakpoint {
719            Style::default().fg(Color::Red)
720        } else {
721            Style::default()
722        }
723        .bg(if is_focused {
724            if is_current {
725                Color::Yellow
726            } else {
727                Color::Green
728            }
729        } else if code_window_focus {
730            Color::Black
731        } else {
732            Color::Reset
733        });
734
735        let mut line = Line::default().patch_style(style).spans([line_text]);
736        line.push_span(Span::styled(
737            control_flow_marker
738                .or(control_flow_marker_pc)
739                .unwrap_or_default(),
740            Style::default().bg(Color::Blue),
741        ));
742        let text = Text::from(line);
743        lines.push(text);
744    }
745
746    for (row, line) in area.rows().zip(lines) {
747        f.render_widget(line, row);
748    }
749}
750
751fn render_registers(
752    f: &mut Frame,
753    area: Rect,
754    cpu: &Cpu,
755    focus: DebuggerFocus,
756    is_editing: bool,
757    edit_buffer: &str,
758) {
759    let mut lines = Vec::new();
760
761    let is_reg_focused = matches!(focus, DebuggerFocus::Reg(_));
762
763    // Helper function to format a register value with optional editing
764    let format_value = |reg: Register, default_value: String| -> String {
765        if is_editing && focus == DebuggerFocus::Reg(reg) {
766            // Show edit buffer with cursor (using underscore)
767            let max_len: usize = match reg {
768                Register::PC | Register::DPTR => 4,
769                _ => 2,
770            };
771            format!("{}_", edit_buffer) + &"_".repeat(max_len.saturating_sub(edit_buffer.len() + 1))
772        } else {
773            default_value
774        }
775    };
776
777    // Helper function to create a styled register value
778    let reg_style = |reg: Register| {
779        if focus == DebuggerFocus::Reg(reg) {
780            if is_editing {
781                Style::default()
782                    .fg(if is_reg_focused {
783                        Color::LightGreen
784                    } else {
785                        Color::Green
786                    })
787                    .add_modifier(Modifier::BOLD | Modifier::UNDERLINED)
788            } else {
789                Style::default()
790                    .fg(if is_reg_focused {
791                        Color::LightYellow
792                    } else {
793                        Color::Yellow
794                    })
795                    .add_modifier(Modifier::BOLD)
796            }
797        } else {
798            Style::default()
799        }
800    };
801
802    // Compact format: PC and SP on one line
803    let sp = cpu.sp();
804    let pc_sp_line = vec![
805        Span::styled(
806            format!(
807                "PC:{}",
808                format_value(Register::PC, format!("{:04X}", cpu.pc))
809            ),
810            reg_style(Register::PC),
811        ),
812        Span::raw(" "),
813        Span::styled(
814            format!("SP:{}", format_value(Register::SP, format!("{:02X}", sp))),
815            reg_style(Register::SP),
816        ),
817        Span::raw(" "),
818        Span::styled(
819            format!(
820                "IP:{}",
821                format_value(Register::IP, format!("{:02X}", cpu.ip()))
822            ),
823            reg_style(Register::IP),
824        ),
825        Span::raw(" "),
826        Span::styled(
827            format!(
828                "IE:{}",
829                format_value(Register::IE, format!("{:02X}", cpu.ie()))
830            ),
831            reg_style(Register::IE),
832        ),
833    ];
834    lines.push(Line::from(pc_sp_line));
835
836    // R0-R7 on one or two lines depending on width
837    let mut r_spans = Vec::new();
838    for i in 0..8 {
839        if i > 0 {
840            r_spans.push(Span::raw(" "));
841        }
842        r_spans.push(Span::styled(
843            format!(
844                "R{}:{}",
845                i,
846                format_value(Register::R(i), format!("{:02X}", cpu.r(i)))
847            ),
848            reg_style(Register::R(i)),
849        ));
850    }
851    lines.push(Line::from(r_spans));
852
853    // A, B, DPTR on one line
854    let abc_line = vec![
855        Span::styled(
856            format!(
857                "A:{}",
858                format_value(Register::A, format!("{:02X}", cpu.a()))
859            ),
860            reg_style(Register::A),
861        ),
862        Span::raw(" "),
863        Span::styled(
864            format!(
865                "B:{}",
866                format_value(Register::B, format!("{:02X}", cpu.b()))
867            ),
868            reg_style(Register::B),
869        ),
870        Span::raw(" "),
871        Span::styled(
872            format!(
873                "DPTR:{}",
874                format_value(Register::DPTR, format!("{:04X}", cpu.dptr()))
875            ),
876            reg_style(Register::DPTR),
877        ),
878    ];
879    lines.push(Line::from(abc_line));
880    lines.push(Line::from(""));
881
882    // PSW flags
883    let mut flag_spans = Vec::new();
884    for (i, flag) in Flag::all().iter().enumerate() {
885        if i > 0 {
886            flag_spans.push(Span::raw(" "));
887        }
888        let style = if cpu.psw(*flag) {
889            Style::default().add_modifier(Modifier::BOLD)
890        } else {
891            Style::default().add_modifier(Modifier::DIM)
892        };
893        flag_spans.push(Span::styled(flag.short_name(), style));
894    }
895    lines.push(Line::from(flag_spans));
896    lines.push(Line::from(""));
897
898    // Internal RAM display (128 bytes)
899    lines.push(Line::from(Span::styled(
900        "Internal RAM:",
901        Style::default().add_modifier(Modifier::BOLD),
902    )));
903
904    // Get current SP value and register bank
905    let sp = cpu.sp() as usize;
906    let register_bank =
907        (if cpu.psw(Flag::RS1) { 2 } else { 0 }) + (if cpu.psw(Flag::RS0) { 1 } else { 0 });
908    let bank_start = register_bank * 8;
909    let bank_end = bank_start + 8;
910
911    // Display 16 bytes per line for 8 lines (128 bytes total)
912    for row in 0..8 {
913        let addr = row * 16;
914        let mut spans = vec![Span::styled(
915            format!("{:02X}: ", addr),
916            Style::default().add_modifier(Modifier::DIM),
917        )];
918
919        for col in 0..16 {
920            let byte_addr = addr + col;
921            let byte_val = cpu.internal_ram[byte_addr];
922            let in_current_register_bank = byte_addr >= bank_start && byte_addr < bank_end;
923
924            // Determine background color based on address
925            let style = if byte_addr == sp.wrapping_add(1) {
926                // Stack pointer: dark green background
927                Style::default().bg(Color::Green).fg(Color::Black)
928            } else if in_current_register_bank {
929                // Current register bank: light yellow background
930                Style::default().bg(Color::Yellow).fg(Color::Black)
931            } else {
932                Style::default()
933            };
934
935            if in_current_register_bank && byte_addr < bank_end - 1 {
936                spans.push(Span::styled(format!("{:02X} ", byte_val), style));
937            } else {
938                spans.push(Span::styled(format!("{:02X}", byte_val), style));
939                spans.push(Span::raw(" "));
940            }
941        }
942
943        lines.push(Line::from(spans));
944    }
945
946    let paragraph = Paragraph::new(lines)
947        .wrap(Wrap { trim: false })
948        .style(Style::default().bg(if is_reg_focused {
949            Color::Black
950        } else {
951            Color::Reset
952        }));
953
954    f.render_widget(paragraph, area);
955}
956
957fn render_output(f: &mut Frame, area: Rect, trace_events: &TracingCollector, focus: DebuggerFocus) {
958    let is_output_focused = matches!(focus, DebuggerFocus::Output);
959
960    // Create header
961    let mut lines = vec![Line::from(Span::styled(
962        "Trace Output:",
963        Style::default().add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
964    ))];
965
966    // Add trace events (most recent at bottom)
967    let available_lines = area.height.saturating_sub(1) as usize; // Account for header
968    trace_events.with_last_n(available_lines, &mut |level: Level, event: &str| {
969        // Color code by log level
970        let level_color = match level {
971            Level::ERROR => Color::Red,
972            Level::WARN => Color::Yellow,
973            Level::INFO => Color::Green,
974            Level::DEBUG => Color::Cyan,
975            Level::TRACE => Color::DarkGray,
976        };
977
978        lines.push(Line::from(vec![Span::styled(
979            event.to_string(),
980            Style::default().fg(level_color),
981        )]));
982    });
983
984    let paragraph = Paragraph::new(lines)
985        .wrap(Wrap { trim: false })
986        .style(Style::default().bg(if is_output_focused {
987            Color::Black
988        } else {
989            Color::Reset
990        }));
991
992    f.render_widget(paragraph, area);
993}
994
995impl Debugger {
996    /// Enter alternate screen and enable raw mode
997    pub fn enter(&mut self) -> io::Result<()> {
998        crossterm::terminal::enable_raw_mode()?;
999        crossterm::execute!(io::stdout(), crossterm::terminal::EnterAlternateScreen,)?;
1000        self.terminal.clear()?;
1001        Ok(())
1002    }
1003
1004    /// Exit alternate screen and disable raw mode
1005    pub fn exit(&mut self) -> io::Result<()> {
1006        crossterm::terminal::disable_raw_mode()?;
1007        crossterm::execute!(io::stdout(), crossterm::terminal::LeaveAlternateScreen,)?;
1008        Ok(())
1009    }
1010}
1011
1012impl Drop for Debugger {
1013    fn drop(&mut self) {
1014        let _ = self.exit();
1015    }
1016}