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, tracing_collector: TracingCollector) -> 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,
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                        && let Ok(value) = u32::from_str_radix(&self.state.edit_buffer, 16)
322                    {
323                        self.apply_register_edit(cpu, ctx, register, value);
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
538#[allow(clippy::too_many_arguments)]
539fn render_frame(
540    f: &mut Frame,
541    cpu: &Cpu,
542    ctx: &mut impl CpuContext,
543    config: &DebuggerConfig,
544    breakpoints: &BTreeSet<u32>,
545    focus: DebuggerFocus,
546    state: DebuggerState,
547    is_editing: bool,
548    edit_buffer: &str,
549    code_window: &CodeWindowState,
550    trace_events: &TracingCollector,
551) {
552    // Create main layout with status bar at bottom
553    let main_layout = Layout::default()
554        .direction(Direction::Vertical)
555        .constraints([
556            Constraint::Min(0),    // Main content area
557            Constraint::Length(1), // Status bar
558        ])
559        .split(f.area());
560
561    let content_area = main_layout[0];
562    let status_area = main_layout[1];
563
564    // Create the main content layout with a vertical separator
565    let main_chunks = Layout::default()
566        .direction(Direction::Horizontal)
567        .constraints([
568            Constraint::Percentage(50),
569            Constraint::Length(1), // Vertical separator
570            Constraint::Percentage(50),
571        ])
572        .split(content_area);
573
574    let right_chunks = Layout::default()
575        .direction(Direction::Vertical)
576        .constraints([Constraint::Min(10), Constraint::Percentage(50)])
577        .split(main_chunks[2]);
578
579    // Render the three panes
580    if state == DebuggerState::Paused {
581        render_disassembly(f, main_chunks[0], cpu, ctx, breakpoints, focus, code_window);
582    }
583
584    // Render vertical separator
585    let separator_lines: Vec<Line> = (0..main_chunks[1].height)
586        .map(|_| Line::from("│"))
587        .collect();
588    let separator = Paragraph::new(separator_lines).style(Style::default().fg(Color::DarkGray));
589    f.render_widget(separator, main_chunks[1]);
590
591    render_registers(f, right_chunks[0], cpu, focus, is_editing, edit_buffer);
592    render_output(f, right_chunks[1], trace_events, focus);
593
594    // Render status bar at the bottom
595    let status_text = format!(
596        " {} │ {}: Step │ {}: Run │ {}: Breakpoint │ Tab/Shift+Tab: Switch │ q: Quit ",
597        if state == DebuggerState::Running {
598            "RUNNING"
599        } else {
600            "PAUSED "
601        },
602        config.step_key_label,
603        config.run_key_label,
604        config.toggle_breakpoint_key_label,
605    );
606
607    let status_style = if state == DebuggerState::Running {
608        Style::default()
609            .bg(Color::Green)
610            .fg(Color::Black)
611            .add_modifier(Modifier::BOLD)
612    } else {
613        Style::default()
614            .bg(Color::Blue)
615            .fg(Color::White)
616            .add_modifier(Modifier::BOLD)
617    };
618
619    let status_bar = Paragraph::new(Line::from(Span::styled(status_text, status_style)));
620    f.render_widget(status_bar, status_area);
621}
622
623fn render_disassembly(
624    f: &mut Frame,
625    area: Rect,
626    cpu: &Cpu,
627    ctx: &mut impl CpuContext,
628    breakpoints: &BTreeSet<u32>,
629    focus: DebuggerFocus,
630    code_window: &CodeWindowState,
631) {
632    let pc = cpu.pc_ext(ctx);
633    let mut lines = Vec::new();
634    let code_window_focus = matches!(focus, DebuggerFocus::Code);
635
636    // Calculate how many instructions we can fit in the available area
637    let available_height = area.height as usize;
638    let max_lines = available_height.min(100);
639    code_window.last_height.set(max_lines);
640
641    // Decode the instructions for the code window, caching the start value if we need to.
642    let focus_addr = code_window.focus.unwrap_or(pc);
643    let mut instructions = cpu.decode_range(ctx, code_window.start.get(), focus_addr, max_lines);
644    if !instructions
645        .iter()
646        .any(|instruction| instruction.pc() == focus_addr)
647    {
648        instructions = cpu.decode_range(ctx, focus_addr.saturating_sub(10), focus_addr, max_lines);
649    }
650    code_window.start.set(instructions.first().unwrap().pc());
651
652    fn control_flow_addr(
653        cpu: &Cpu,
654        ctx: &impl CpuContext,
655        control_flow: ControlFlow,
656    ) -> Option<(&'static str, u32)> {
657        match control_flow {
658            ControlFlow::Call(_, pc) => Some((" ↦ ", cpu.pc_ext_addr(ctx, pc))),
659            ControlFlow::Choice(_, pc) => Some((" ↔ ", cpu.pc_ext_addr(ctx, pc))),
660            _ => None,
661        }
662    }
663
664    let control_flow = instructions
665        .iter()
666        .find(|instruction| instruction.pc() == focus_addr)
667        .map(|instruction| instruction.control_flow())
668        .and_then(|control_flow| control_flow_addr(cpu, ctx, control_flow));
669    let control_flow_pc = instructions
670        .iter()
671        .find(|instruction| instruction.pc() == pc)
672        .map(|instruction| instruction.control_flow())
673        .and_then(|control_flow| control_flow_addr(cpu, ctx, control_flow));
674
675    for instruction in instructions {
676        let bytes_str = instruction
677            .bytes()
678            .iter()
679            .map(|b| format!("{:02X}", b))
680            .collect::<Vec<_>>()
681            .join(" ");
682
683        let current_pc = instruction.pc();
684        let has_breakpoint = breakpoints.contains(&current_pc);
685        let is_current = current_pc == pc;
686        let is_focused = current_pc == focus_addr;
687
688        let bp_marker = if has_breakpoint { "●" } else { " " };
689        let pc_marker = if is_current { ">" } else { " " };
690
691        let control_flow_marker = control_flow.and_then(|(marker, addr)| {
692            if addr == current_pc {
693                Some(marker)
694            } else {
695                None
696            }
697        });
698        let control_flow_marker_pc = control_flow_pc.and_then(|(marker, addr)| {
699            if addr == current_pc {
700                Some(marker)
701            } else {
702                None
703            }
704        });
705
706        let line_text = format!(
707            "{}{} {:04X}: {:12} {} ",
708            bp_marker,
709            pc_marker,
710            current_pc,
711            bytes_str,
712            instruction.decode(),
713        );
714
715        let style = if is_current {
716            Style::default()
717                .fg(Color::Yellow)
718                .add_modifier(Modifier::BOLD)
719        } else if has_breakpoint {
720            Style::default().fg(Color::Red)
721        } else {
722            Style::default()
723        }
724        .bg(if is_focused {
725            if is_current {
726                Color::Yellow
727            } else {
728                Color::Green
729            }
730        } else if code_window_focus {
731            Color::Black
732        } else {
733            Color::Reset
734        });
735
736        let mut line = Line::default().patch_style(style).spans([line_text]);
737        line.push_span(Span::styled(
738            control_flow_marker
739                .or(control_flow_marker_pc)
740                .unwrap_or_default(),
741            Style::default().bg(Color::Blue),
742        ));
743        let text = Text::from(line);
744        lines.push(text);
745    }
746
747    for (row, line) in area.rows().zip(lines) {
748        f.render_widget(line, row);
749    }
750}
751
752fn render_registers(
753    f: &mut Frame,
754    area: Rect,
755    cpu: &Cpu,
756    focus: DebuggerFocus,
757    is_editing: bool,
758    edit_buffer: &str,
759) {
760    let mut lines = Vec::new();
761
762    let is_reg_focused = matches!(focus, DebuggerFocus::Reg(_));
763
764    // Helper function to format a register value with optional editing
765    let format_value = |reg: Register, default_value: String| -> String {
766        if is_editing && focus == DebuggerFocus::Reg(reg) {
767            // Show edit buffer with cursor (using underscore)
768            let max_len: usize = match reg {
769                Register::PC | Register::DPTR => 4,
770                _ => 2,
771            };
772            format!("{}_", edit_buffer) + &"_".repeat(max_len.saturating_sub(edit_buffer.len() + 1))
773        } else {
774            default_value
775        }
776    };
777
778    // Helper function to create a styled register value
779    let reg_style = |reg: Register| {
780        if focus == DebuggerFocus::Reg(reg) {
781            if is_editing {
782                Style::default()
783                    .fg(if is_reg_focused {
784                        Color::LightGreen
785                    } else {
786                        Color::Green
787                    })
788                    .add_modifier(Modifier::BOLD | Modifier::UNDERLINED)
789            } else {
790                Style::default()
791                    .fg(if is_reg_focused {
792                        Color::LightYellow
793                    } else {
794                        Color::Yellow
795                    })
796                    .add_modifier(Modifier::BOLD)
797            }
798        } else {
799            Style::default()
800        }
801    };
802
803    // Compact format: PC and SP on one line
804    let sp = cpu.sp();
805    let pc_sp_line = vec![
806        Span::styled(
807            format!(
808                "PC:{}",
809                format_value(Register::PC, format!("{:04X}", cpu.pc))
810            ),
811            reg_style(Register::PC),
812        ),
813        Span::raw(" "),
814        Span::styled(
815            format!("SP:{}", format_value(Register::SP, format!("{:02X}", sp))),
816            reg_style(Register::SP),
817        ),
818        Span::raw(" "),
819        Span::styled(
820            format!(
821                "IP:{}",
822                format_value(Register::IP, format!("{:02X}", cpu.ip()))
823            ),
824            reg_style(Register::IP),
825        ),
826        Span::raw(" "),
827        Span::styled(
828            format!(
829                "IE:{}",
830                format_value(Register::IE, format!("{:02X}", cpu.ie()))
831            ),
832            reg_style(Register::IE),
833        ),
834    ];
835    lines.push(Line::from(pc_sp_line));
836
837    // R0-R7 on one or two lines depending on width
838    let mut r_spans = Vec::new();
839    for i in 0..8 {
840        if i > 0 {
841            r_spans.push(Span::raw(" "));
842        }
843        r_spans.push(Span::styled(
844            format!(
845                "R{}:{}",
846                i,
847                format_value(Register::R(i), format!("{:02X}", cpu.r(i)))
848            ),
849            reg_style(Register::R(i)),
850        ));
851    }
852    lines.push(Line::from(r_spans));
853
854    // A, B, DPTR on one line
855    let abc_line = vec![
856        Span::styled(
857            format!(
858                "A:{}",
859                format_value(Register::A, format!("{:02X}", cpu.a()))
860            ),
861            reg_style(Register::A),
862        ),
863        Span::raw(" "),
864        Span::styled(
865            format!(
866                "B:{}",
867                format_value(Register::B, format!("{:02X}", cpu.b()))
868            ),
869            reg_style(Register::B),
870        ),
871        Span::raw(" "),
872        Span::styled(
873            format!(
874                "DPTR:{}",
875                format_value(Register::DPTR, format!("{:04X}", cpu.dptr()))
876            ),
877            reg_style(Register::DPTR),
878        ),
879    ];
880    lines.push(Line::from(abc_line));
881    lines.push(Line::from(""));
882
883    // PSW flags
884    let mut flag_spans = Vec::new();
885    for (i, flag) in Flag::all().iter().enumerate() {
886        if i > 0 {
887            flag_spans.push(Span::raw(" "));
888        }
889        let style = if cpu.psw(*flag) {
890            Style::default().add_modifier(Modifier::BOLD)
891        } else {
892            Style::default().add_modifier(Modifier::DIM)
893        };
894        flag_spans.push(Span::styled(flag.short_name(), style));
895    }
896    lines.push(Line::from(flag_spans));
897    lines.push(Line::from(""));
898
899    // Internal RAM display (128 bytes)
900    lines.push(Line::from(Span::styled(
901        "Internal RAM:",
902        Style::default().add_modifier(Modifier::BOLD),
903    )));
904
905    // Get current SP value and register bank
906    let sp = cpu.sp() as usize;
907    let register_bank =
908        (if cpu.psw(Flag::RS1) { 2 } else { 0 }) + (if cpu.psw(Flag::RS0) { 1 } else { 0 });
909    let bank_start = register_bank * 8;
910    let bank_end = bank_start + 8;
911
912    // Display 16 bytes per line for 8 lines (128 bytes total)
913    for row in 0..8 {
914        let addr = row * 16;
915        let mut spans = vec![Span::styled(
916            format!("{:02X}: ", addr),
917            Style::default().add_modifier(Modifier::DIM),
918        )];
919
920        for col in 0..16 {
921            let byte_addr = addr + col;
922            let byte_val = cpu.internal_ram[byte_addr];
923            let in_current_register_bank = byte_addr >= bank_start && byte_addr < bank_end;
924
925            // Determine background color based on address
926            let style = if byte_addr == sp.wrapping_add(1) {
927                // Stack pointer: dark green background
928                Style::default().bg(Color::Green).fg(Color::Black)
929            } else if in_current_register_bank {
930                // Current register bank: light yellow background
931                Style::default().bg(Color::Yellow).fg(Color::Black)
932            } else {
933                Style::default()
934            };
935
936            if in_current_register_bank && byte_addr < bank_end - 1 {
937                spans.push(Span::styled(format!("{:02X} ", byte_val), style));
938            } else {
939                spans.push(Span::styled(format!("{:02X}", byte_val), style));
940                spans.push(Span::raw(" "));
941            }
942        }
943
944        lines.push(Line::from(spans));
945    }
946
947    let paragraph = Paragraph::new(lines)
948        .wrap(Wrap { trim: false })
949        .style(Style::default().bg(if is_reg_focused {
950            Color::Black
951        } else {
952            Color::Reset
953        }));
954
955    f.render_widget(paragraph, area);
956}
957
958fn render_output(f: &mut Frame, area: Rect, trace_events: &TracingCollector, focus: DebuggerFocus) {
959    let is_output_focused = matches!(focus, DebuggerFocus::Output);
960
961    // Create header
962    let mut lines = vec![Line::from(Span::styled(
963        "Trace Output:",
964        Style::default().add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
965    ))];
966
967    // Add trace events (most recent at bottom)
968    let available_lines = area.height.saturating_sub(1) as usize; // Account for header
969    trace_events.with_last_n(available_lines, &mut |level: Level, event: &str| {
970        // Color code by log level
971        let level_color = match level {
972            Level::ERROR => Color::Red,
973            Level::WARN => Color::Yellow,
974            Level::INFO => Color::Green,
975            Level::DEBUG => Color::Cyan,
976            Level::TRACE => Color::DarkGray,
977        };
978
979        lines.push(Line::from(vec![Span::styled(
980            event.to_string(),
981            Style::default().fg(level_color),
982        )]));
983    });
984
985    let paragraph = Paragraph::new(lines)
986        .wrap(Wrap { trim: false })
987        .style(Style::default().bg(if is_output_focused {
988            Color::Black
989        } else {
990            Color::Reset
991        }));
992
993    f.render_widget(paragraph, area);
994}
995
996impl Debugger {
997    /// Enter alternate screen and enable raw mode
998    pub fn enter(&mut self) -> io::Result<()> {
999        crossterm::terminal::enable_raw_mode()?;
1000        crossterm::execute!(io::stdout(), crossterm::terminal::EnterAlternateScreen,)?;
1001        self.terminal.clear()?;
1002        Ok(())
1003    }
1004
1005    /// Exit alternate screen and disable raw mode
1006    pub fn exit(&mut self) -> io::Result<()> {
1007        crossterm::terminal::disable_raw_mode()?;
1008        crossterm::execute!(io::stdout(), crossterm::terminal::LeaveAlternateScreen,)?;
1009        Ok(())
1010    }
1011}
1012
1013impl Drop for Debugger {
1014    fn drop(&mut self) {
1015        let _ = self.exit();
1016    }
1017}