diff --git a/Cargo.lock b/Cargo.lock index b5b74c6..9566688 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -271,6 +271,7 @@ dependencies = [ "clap", "crossterm", "ratatui", + "strum", "tracing", "tracing-subscriber", "verilated", diff --git a/Cargo.toml b/Cargo.toml index 5180354..62ffd01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ tracing = "0.1.40" tracing-subscriber = { version = "0.3", features = ["env-filter"] } crossterm = "0.27.0" ratatui = "0.26.2" +strum = "0.26.2" [build-dependencies] verilator = { path = "./dependencies/verilated-rs/verilator", features = ["gen", "module"] } diff --git a/src/emulator.rs b/src/emulator.rs index 39b5db9..0226da5 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -8,10 +8,6 @@ use crate::dut::Dut; use crate::exception::Trap; use crate::tui::{Tui, UI}; use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind}; -use ratatui::{ - prelude::*, - widgets::{block::*, *}, -}; use std::io; #[derive(Default)] @@ -81,7 +77,9 @@ impl Emulator { fn quit(&mut self, terminal: &mut Tui) { while !self.ui.cmd.exit { - terminal.draw(|frame| self.render_frame(frame)).unwrap(); + terminal + .draw(|frame| frame.render_widget(&self.ui, frame.size())) + .unwrap(); self.handle_events().unwrap(); } } @@ -123,81 +121,12 @@ impl Emulator { } } - fn render_frame(&self, frame: &mut Frame) { - // layout - let layout_vertical = Layout::default() - .direction(Direction::Vertical) - .constraints(vec![ - Constraint::Percentage(40), - Constraint::Percentage(40), - Constraint::Percentage(20), - ]) - .split(frame.size()); - - let layout_horizontal = Layout::default() - .direction(Direction::Horizontal) - .constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(layout_vertical[1]); - - // render - frame.render_widget( - Paragraph::new(self.ui.buffer.inst.to_string()) - .block( - Block::bordered() - .title("Instructions") - .title_alignment(Alignment::Left) - .border_type(BorderType::Rounded), - ) - .style(Style::default().fg(Color::Cyan)) - .left_aligned(), - layout_vertical[0], - ); - - frame.render_widget( - Paragraph::new(self.ui.buffer.cpu.to_string()) - .block( - Block::bordered() - .title("CPU") - .title_alignment(Alignment::Left) - .border_type(BorderType::Rounded), - ) - .style(Style::default().fg(Color::Cyan)) - .left_aligned(), - layout_horizontal[0], - ); - - frame.render_widget( - Paragraph::new(self.ui.buffer.dut.to_string()) - .block( - Block::bordered() - .title("DUT") - .title_alignment(Alignment::Left) - .border_type(BorderType::Rounded), - ) - .style(Style::default().fg(Color::Cyan)) - .left_aligned(), - layout_horizontal[1], - ); - - frame.render_widget( - Paragraph::new(self.ui.buffer.diff.to_string()) - .block( - Block::bordered() - .title("Difftest Status") - .title_alignment(Alignment::Center) - .border_type(BorderType::Rounded), - ) - .style(Style::default().fg(Color::Cyan)) - .centered(), - layout_vertical[2], - ); - } - fn handle_key_event(&mut self, key_event: KeyEvent) { match key_event.code { KeyCode::Char('q') | KeyCode::Char('Q') => self.exit(), KeyCode::Char('c') | KeyCode::Char('C') => self.r#continue(), - + KeyCode::Char('h') | KeyCode::Left => self.ui.previous_tab(), + KeyCode::Char('l') | KeyCode::Right => self.ui.next_tab(), _ => {} } } @@ -251,7 +180,7 @@ impl Emulator { match self.cpu.gpr.record { Some((wnum, wdata)) => { cpu_diff = DebugInfo::new(true, pc, wnum, wdata); - info!("[cpu] record: true, pc: {:#x}, inst: {}", pc, self.cpu.inst); + info!("[cpu] record: true, pc: {:#x}, inst: {}", pc, self.cpu.inst); break; } None => { @@ -329,7 +258,7 @@ impl Emulator { /// Start executing the emulator with difftest and tui. pub fn start_diff_tui(&mut self, terminal: &mut Tui) { - self.ui.buffer.diff.push("running".to_string()); + self.ui.selected_tab.ui_buffer.diff.push("running".to_string()); let mut last_diff = DebugInfo::default(); while !self.ui.cmd.exit { @@ -341,7 +270,8 @@ impl Emulator { self .ui - .buffer + .selected_tab + .ui_buffer .inst .push(format!("pc: {:#x}, inst: {}", pc, self.cpu.inst)); @@ -349,7 +279,8 @@ impl Emulator { Trap::Fatal => { self .ui - .buffer + .selected_tab + .ui_buffer .diff .push(format!("fatal pc: {:#x}, trap {:#?}", self.cpu.pc, trap)); @@ -365,7 +296,8 @@ impl Emulator { cpu_diff = DebugInfo::new(true, pc, wnum, wdata); self .ui - .buffer + .selected_tab + .ui_buffer .cpu .push(format!("record: true, pc: {:#x}, inst: {}", pc, self.cpu.inst)); break; @@ -373,7 +305,8 @@ impl Emulator { None => { self .ui - .buffer + .selected_tab + .ui_buffer .cpu .push(format!("record: false, pc: {:#x}, inst: {}", pc, self.cpu.inst)); } @@ -396,7 +329,7 @@ impl Emulator { // should be `Exception::InstructionAccessFault`. dut.data = self.cpu.bus.read(p_addr, crate::cpu::DOUBLEWORD).unwrap(); - self.ui.buffer.dut.push(format!( + self.ui.selected_tab.ui_buffer.dut.push(format!( "{}, data_sram: addr: {:#x}, data: {:#018x}", dut.ticks, data_sram.addr, dut.data )) @@ -412,7 +345,7 @@ impl Emulator { // should be `Exception::InstructionAccessFault`. dut.inst = self.cpu.bus.read(p_pc, crate::cpu::WORD).unwrap() as u32; - self.ui.buffer.dut.push(format!( + self.ui.selected_tab.ui_buffer.dut.push(format!( "{}, inst_sram: pc: {:#x}, inst: {:#010x}", dut.ticks, inst_sram.addr, dut.inst )) @@ -423,7 +356,7 @@ impl Emulator { break; } } - self.ui.buffer.dut.push(format!( + self.ui.selected_tab.ui_buffer.dut.push(format!( "{}, pc: {:#010x} wnum: {} wdata: {:#018x}", dut.ticks, dut.top.debug_pc(), @@ -433,16 +366,17 @@ impl Emulator { // ==================== diff ==================== if cpu_diff != dut_diff { - self.ui.buffer.diff.clear(); + self.ui.selected_tab.ui_buffer.diff.clear(); self .ui - .buffer + .selected_tab + .ui_buffer .diff .push("difftest failed. press 'q' or 'Q' to quit. ".to_string()); - self.ui.buffer.diff.push(format!("last: {}", last_diff)); - self.ui.buffer.diff.push(format!("cpu : {}", cpu_diff)); - self.ui.buffer.diff.push(format!("dut : {}", dut_diff)); + self.ui.selected_tab.ui_buffer.diff.push(format!("last: {}", last_diff)); + self.ui.selected_tab.ui_buffer.diff.push(format!("cpu : {}", cpu_diff)); + self.ui.selected_tab.ui_buffer.diff.push(format!("dut : {}", dut_diff)); self.quit(terminal); @@ -451,7 +385,9 @@ impl Emulator { last_diff = cpu_diff; // tui - terminal.draw(|frame| self.render_frame(frame)).unwrap(); + terminal + .draw(|frame| frame.render_widget(&self.ui, frame.size())) + .unwrap(); if !self.ui.cmd.r#continue { self.handle_events().unwrap(); } diff --git a/src/main.rs b/src/main.rs index b2d5432..01904f4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -66,16 +66,16 @@ fn main() -> anyhow::Result<()> { emu.initialize_disk(img_data); emu.initialize_pc(DRAM_BASE); - let mut terminal = tui::init()?; - match (args.diff, args.tui) { - (true, true) => emu.start_diff_tui(&mut terminal), + (true, true) => { + let mut terminal = tui::init()?; + emu.start_diff_tui(&mut terminal); + tui::restore()?; + } (true, false) => emu.start_diff(), (false, false) => emu.start(), _ => panic!("Tui without difftest is not supported yet."), } - tui::restore()?; - Ok(()) } diff --git a/src/tui.rs b/src/tui.rs index 2ce2669..490f468 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -2,7 +2,8 @@ use std::io::{self, stdout, Stdout}; use std::{collections::VecDeque, fmt}; use crossterm::{execute, terminal::*}; -use ratatui::prelude::*; +use ratatui::{prelude::*, style::palette::tailwind, widgets::*}; +use strum::{Display, EnumIter, FromRepr, IntoEnumIterator}; /// A type alias for the terminal type used in this application pub type Tui = Terminal>; @@ -26,13 +27,13 @@ const CPU_BUFFER_SIZE: usize = 10; const DUT_BUFFER_SIZE: usize = 10; const DIFF_BUFFER_SIZE: usize = 5; -#[derive(Default)] -pub struct Buffer { +#[derive(Clone)] +pub struct InfoBuffer { info: VecDeque, size: usize, } -impl Buffer { +impl InfoBuffer { fn new(size: usize) -> Self { Self { info: VecDeque::new(), @@ -52,7 +53,7 @@ impl Buffer { } } -impl fmt::Display for Buffer { +impl fmt::Display for InfoBuffer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for info in &self.info { write!(f, "{}\n", info)?; @@ -61,43 +62,228 @@ impl fmt::Display for Buffer { } } -#[derive(Default)] +#[derive(Clone)] pub struct UIBuffer { - pub inst: Buffer, - pub cpu: Buffer, - pub dut: Buffer, - pub diff: Buffer, + pub inst: InfoBuffer, + pub cpu: InfoBuffer, + pub dut: InfoBuffer, + pub diff: InfoBuffer, } impl UIBuffer { pub fn new() -> Self { UIBuffer { - inst: Buffer::new(INST_BUFFER_SIZE), - cpu: Buffer::new(CPU_BUFFER_SIZE), - dut: Buffer::new(DUT_BUFFER_SIZE), - diff: Buffer::new(DIFF_BUFFER_SIZE), + inst: InfoBuffer::new(INST_BUFFER_SIZE), + cpu: InfoBuffer::new(CPU_BUFFER_SIZE), + dut: InfoBuffer::new(DUT_BUFFER_SIZE), + diff: InfoBuffer::new(DIFF_BUFFER_SIZE), } } } +#[derive(Default)] pub struct UICommand { pub r#continue: bool, pub exit: bool, } +#[derive(Default, Clone, Copy, Display, FromRepr, EnumIter)] +pub enum SelectedTabEnum { + #[default] + #[strum(to_string = "Main")] + Main, + #[strum(to_string = "Trace")] + Trace, + #[strum(to_string = "Difftest")] + Difftest, +} + +#[derive(Clone)] +pub struct SelectedTab { + pub ui_buffer: UIBuffer, + state: SelectedTabEnum, +} + +impl SelectedTab { + pub fn new() -> Self { + SelectedTab { + ui_buffer: UIBuffer::new(), + state: SelectedTabEnum::default(), + } + } +} + +impl SelectedTabEnum { + fn title(self) -> Line<'static> { + format!(" {self} ") + .fg(tailwind::SLATE.c200) + .bg(self.palette().c900) + .into() + } + + /// Get the previous tab, if there is no previous tab return the current tab. + pub fn previous(self) -> Self { + let current_index: usize = self as usize; + let previous_index = current_index.saturating_sub(1); + Self::from_repr(previous_index).unwrap_or(self) + } + + /// Get the next tab, if there is no next tab return the current tab. + pub fn next(self) -> Self { + let current_index = self as usize; + let next_index = current_index.saturating_add(1); + Self::from_repr(next_index).unwrap_or(self) + } + + const fn palette(self) -> tailwind::Palette { + match self { + Self::Main => tailwind::BLUE, + Self::Trace => tailwind::EMERALD, + Self::Difftest => tailwind::INDIGO, + } + } +} + +impl Widget for SelectedTab { + fn render(self, area: Rect, buf: &mut Buffer) { + // in a real app these might be separate widgets + match self.state { + SelectedTabEnum::Main => self.render_main(area, buf), + SelectedTabEnum::Trace => self.render_trace(area, buf), + SelectedTabEnum::Difftest => self.render_difftest(area, buf), + } + } +} + +impl SelectedTab { + fn render_main(self, area: Rect, buf: &mut Buffer) { + Paragraph::new("Welcome to the Ratatui tabs example!") + .block(self.block()) + .render(area, buf); + } + + fn render_trace(self, area: Rect, buf: &mut Buffer) { + Paragraph::new("Look! I'm different than others!") + .block(self.block()) + .render(area, buf); + } + + fn render_difftest(self, area: Rect, buf: &mut Buffer) { + // layout + let layout_vertical = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![ + Constraint::Percentage(40), + Constraint::Percentage(40), + Constraint::Percentage(20), + ]) + .split(area); + + let layout_horizontal = Layout::default() + .direction(Direction::Horizontal) + .constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(layout_vertical[1]); + + // render_frame + Paragraph::new(self.ui_buffer.inst.to_string()) + .block( + Block::bordered() + .title("Instructions") + .title_alignment(Alignment::Left) + .border_type(BorderType::Rounded), + ) + .style(Style::default().fg(Color::Cyan)) + .left_aligned() + .render(layout_vertical[0], buf); + + Paragraph::new(self.ui_buffer.cpu.to_string()) + .block( + Block::bordered() + .title("CPU") + .title_alignment(Alignment::Left) + .border_type(BorderType::Rounded), + ) + .style(Style::default().fg(Color::Cyan)) + .left_aligned() + .render(layout_horizontal[0], buf); + + Paragraph::new(self.ui_buffer.dut.to_string()) + .block( + Block::bordered() + .title("DUT") + .title_alignment(Alignment::Left) + .border_type(BorderType::Rounded), + ) + .style(Style::default().fg(Color::Cyan)) + .left_aligned() + .render(layout_horizontal[1], buf); + + Paragraph::new(self.ui_buffer.diff.to_string()) + .block( + Block::bordered() + .title("Difftest Status") + .title_alignment(Alignment::Center) + .border_type(BorderType::Rounded), + ) + .style(Style::default().fg(Color::Cyan)) + .centered() + .render(layout_vertical[2], buf) + } + + /// A block surrounding the tab's content + fn block(self) -> Block<'static> { + Block::bordered() + .border_set(symbols::border::PROPORTIONAL_TALL) + .padding(Padding::horizontal(1)) + .border_style(self.state.palette().c700) + } +} + pub struct UI { - pub buffer: UIBuffer, pub cmd: UICommand, + pub selected_tab: SelectedTab, } impl UI { pub fn new() -> Self { Self { - buffer: UIBuffer::new(), - cmd: UICommand { - r#continue: false, - exit: false, - }, + cmd: UICommand::default(), + selected_tab: SelectedTab::new(), } } + + pub fn next_tab(&mut self) { + self.selected_tab.state = self.selected_tab.state.next(); + } + + pub fn previous_tab(&mut self) { + self.selected_tab.state = self.selected_tab.state.previous(); + } + + fn render_tabs(&self, area: Rect, buf: &mut Buffer) { + let titles = SelectedTabEnum::iter().map(SelectedTabEnum::title); + let highlight_style = (Color::default(), self.selected_tab.state.palette().c700); + let selected_tab_index = self.selected_tab.state as usize; + Tabs::new(titles) + .highlight_style(highlight_style) + .select(selected_tab_index) + .padding("", "") + .divider(" ") + .render(area, buf); + } +} + +impl Widget for &UI { + fn render(self, area: Rect, buf: &mut Buffer) { + let layout_vertical = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![ + Constraint::Percentage(5), + Constraint::Percentage(95), + ]) + .split(area); + + self.render_tabs(layout_vertical[0], buf); + self.selected_tab.clone().render(layout_vertical[1], buf); + } }