sylveos

Toy Operating System
Log | Files | Refs

commit 4e216bc7abed8d7f0143c87bbb4dc8a8d39dcbb2
parent da41585bef676857b6810f6ae680dd30519175d2
Author: Sylvia Ivory <git@sivory.net>
Date:   Mon, 16 Mar 2026 14:24:44 -0700

Add register view

Diffstat:
Msylveos/journal.zig | 19+++++++++++++++++++
Msylveos/syscall.zig | 3++-
Mtools/src/sylveos/journal.rs | 303++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mtools/src/sylveos/mod.rs | 35++++++++++++++++++++++++++++++-----
Mtools/src/sylveos/paragraph_view.rs | 6+++---
5 files changed, 309 insertions(+), 57 deletions(-)

diff --git a/sylveos/journal.zig b/sylveos/journal.zig @@ -2,12 +2,14 @@ const std = @import("std"); const pi = @import("pi"); const uart = pi.devices.mini_uart; +const interrupts = pi.interrupts; const JOURNAL_START: u8 = 0x1; const JOURNAL_START_SENTINEL: u8 = 0x2; const JOURNAL_END_SENTINEL: u8 = 0x2; const JOURNAL_APPEND_LAST_START: u8 = 0x3; const JOURNAL_APPEND_LAST_END: u8 = 0x3; +const JOURNAL_APPEND_LAST_REGISTERS: u8 = 0x4; pub const LogLevel = enum(u8) { Critical = 0, @@ -54,6 +56,23 @@ pub fn append(comptime fmt: []const u8, args: anytype) void { uart.flush(); } +fn write_u32(v: u32) void { + var slice_len: [4]u8 = undefined; + std.mem.writeInt(u32, &slice_len, v, .little); + uart.write_slice(&slice_len); +} + +pub fn append_registers(regs: *const interrupts.Registers) void { + uart.write_byte(JOURNAL_APPEND_LAST_REGISTERS); + for (regs.gp) |r| { + write_u32(r); + } + write_u32(regs.sp); + write_u32(regs.lr); + write_u32(regs.pc); + write_u32(@bitCast(regs.psr)); +} + pub fn critical(comptime fmt: []const u8, args: anytype) void { print(.Critical, fmt, args); } diff --git a/sylveos/syscall.zig b/sylveos/syscall.zig @@ -331,12 +331,13 @@ pub fn syscall_handler(registers: interrupts.Registers, _: interrupts.ExceptionV regs.gp[0] = result; }, else => { - journal.warn("[syscall]: UNHANDLED({d})", .{n}); + journal.warn("[syscall] UNHANDLED({d})", .{n}); regs.gp[0] = @bitCast(@as(i32, -1)); }, } journal.append(" = 0x{X}", .{regs.gp[0]}); + journal.append_registers(&regs); regs.pc += 4; last_registers = regs; switching.restore_state(&last_registers); diff --git a/tools/src/sylveos/journal.rs b/tools/src/sylveos/journal.rs @@ -6,19 +6,18 @@ use std::sync::{ use chrono::{DateTime, Local}; use ratatui::{ buffer::Buffer, - layout::Rect, - style::Stylize, - text::{Line, Span}, - widgets::Widget, + layout::{Constraint, Rect}, + style::{Color, Style, Stylize}, + text::{Span, ToSpan}, + widgets::{Block, Borders, Clear, Row, StatefulWidget, Table, TableState, Widget}, }; -use crate::sylveos::{ParagraphView, ParagraphViewState}; - #[derive(Debug, Clone)] pub struct JournalWidget { state: Arc<std::sync::RwLock<JournalState>>, - paragraph_state: Arc<std::sync::RwLock<ParagraphViewState>>, + table_state: Arc<std::sync::RwLock<TableState>>, disconnected: Arc<AtomicBool>, + popup_shown: Arc<AtomicBool>, } #[derive(Debug, Default)] @@ -30,7 +29,8 @@ impl JournalWidget { pub fn new(disconnected: Arc<AtomicBool>) -> Self { Self { state: Arc::new(std::sync::RwLock::new(JournalState::default())), - paragraph_state: Arc::new(std::sync::RwLock::new(ParagraphViewState::default())), + table_state: Arc::new(std::sync::RwLock::new(TableState::default())), + popup_shown: Arc::new(AtomicBool::new(false)), disconnected, } } @@ -38,6 +38,46 @@ impl JournalWidget { pub fn state(&self) -> Arc<std::sync::RwLock<JournalState>> { self.state.clone() } + + pub fn popup_shown(&self) -> bool { + self.popup_shown.load(Ordering::Relaxed) + } + + pub fn set_popup_shown(&self, v: bool) { + self.popup_shown.store(v, Ordering::Release); + } + + pub fn scroll_up(&self) { + if self.popup_shown() { + return; + } + let mut state = self.table_state.write().unwrap(); + state.select_previous(); + } + + pub fn scroll_down(&self) { + if self.popup_shown() { + return; + } + let mut state = self.table_state.write().unwrap(); + state.select_next(); + } + + pub fn scroll_to_top(&self) { + if self.popup_shown() { + return; + } + let mut state = self.table_state.write().unwrap(); + state.select_first(); + } + + pub fn scroll_to_bottom(&self) { + if self.popup_shown() { + return; + } + let mut state = self.table_state.write().unwrap(); + state.select_last(); + } } impl JournalState { @@ -50,51 +90,150 @@ impl JournalState { last.msg.push_str(&msg); } } -} - -impl ParagraphView for JournalWidget { - fn get_view_state(&self) -> Arc<std::sync::RwLock<ParagraphViewState>> { - self.paragraph_state.clone() - } - - fn get_title(&self) -> String { - String::from("Journal") - } - fn get_subtitle(&self) -> Option<String> { - let disconnected = self.disconnected.load(Ordering::Relaxed); - - if disconnected { - Some(String::from("Disconnected")) - } else { - None + pub fn append_regs(&mut self, regs: Vec<u32>) { + if let Some(last) = self.lines.last_mut() { + last.registers = Some(regs.try_into().unwrap()) } } +} - fn get_paragraph_contents(&self) -> Vec<Line<'_>> { +impl Widget for &JournalWidget { + fn render(self, area: Rect, buf: &mut Buffer) { let state = self.state.read().unwrap(); + let mut table_state = self.table_state.write().unwrap(); + let mut block = Block::bordered().title("Journal"); + + if self.disconnected.load(Ordering::Relaxed) { + block = block.title_bottom("Disconnected"); + } - state + let rows = state .lines .iter() - .map(|e| { - let timestamp = e.time.format("%H:%M:%S%.3f"); - - Line::from(vec![ - timestamp.to_string().white(), - Span::from(" "), - e.level.repr(), - Span::from(" "), - Span::from(e.msg.clone()), + .map(|l| { + let time = l.time.format("%H:%M:%S.3f").to_string(); + Row::new([ + time.white(), + l.level.repr(), + l.sub_system.to_span(), + l.msg.to_span(), + if l.registers.is_some() { + "Y".to_span() + } else { + "N".to_span() + }, ]) }) - .collect() - } -} + .collect::<Vec<_>>(); -impl Widget for &JournalWidget { - fn render(self, area: Rect, buf: &mut Buffer) { - self.render_paragraph(area, buf); + let widths = [ + Constraint::Percentage(10), + Constraint::Percentage(5), + Constraint::Percentage(5), + Constraint::Percentage(75), + Constraint::Percentage(5), + ]; + + let header = Row::new(["Time", "Level", "Subsystem", "Message", "Registers"]) + .style(Style::new().bold()) + .bottom_margin(1); + + let table = Table::new(rows, widths) + .header(header) + .column_spacing(1) + .style(Color::White) + .block(block) + .row_highlight_style(Style::new().on_black().bold()); + + StatefulWidget::render(table, area, buf, &mut table_state); + + if self.popup_shown() { + if let Some(idx) = table_state.selected() { + if let Some(Some(registers)) = state.lines.get(idx).map(|l| l.registers) { + let popup_width = area.width * 20 / 100; + let popup_height = area.height * 70 / 100; + let popup_x = area.x + (area.width - popup_width) / 2; + let popup_y = area.y + (area.height - popup_height) / 2; + + let popup_area = Rect { + x: popup_x, + y: popup_y, + width: popup_width, + height: popup_height, + }; + + Clear.render(popup_area, buf); + + let available_height = popup_height.saturating_sub(4); // 2 for borders, 2 for header + let max_rows_per_column = available_height.max(1) as usize; + + let total_registers = registers.len(); + let num_column_pairs = + (total_registers + max_rows_per_column - 1) / max_rows_per_column; + + let formatted = (0..max_rows_per_column) + .map(|row_idx| { + let mut cells = Vec::new(); + + for col_pair in 0..num_column_pairs { + let reg_idx = col_pair * max_rows_per_column + row_idx; + + if reg_idx < total_registers { + cells.push(reg_name(reg_idx).to_string()); + cells.push(format!("0x{:08X}", registers[reg_idx])); + } else { + cells.push("".to_string()); + cells.push("".to_string()); + } + } + + cells + }) + .collect::<Vec<_>>(); + + let register_rows = formatted + .iter() + .map(|r| Row::new(r.iter().cloned())) + .collect::<Vec<_>>(); + + let mut header_cells = Vec::new(); + for _ in 0..num_column_pairs { + header_cells.push("Reg"); + header_cells.push("Value"); + } + + let width_per_pair = 100 / num_column_pairs as u16; + let mut widths = Vec::new(); + for _ in 0..num_column_pairs { + widths.push(Constraint::Percentage(width_per_pair * 25 / 100)); + widths.push(Constraint::Percentage(width_per_pair * 75 / 100)); + } + + let block = Block::new() + .title("Registers") + .borders(Borders::ALL) + .style(Style::default().bg(Color::Black)); + + let register_header = Row::new(header_cells) + .style(Style::new().bold()) + .bottom_margin(1); + + let register_table = Table::new(register_rows, widths) + .header(register_header) + .column_spacing(1) + .style(Color::White) + .block(block) + .row_highlight_style(Style::new().on_black().bold()); + + StatefulWidget::render(register_table, popup_area, buf, &mut TableState::new()); + } else { + self.popup_shown.store(false, Ordering::Release); + } + } else { + self.popup_shown.store(false, Ordering::Release); + } + } } } @@ -102,7 +241,52 @@ impl Widget for &JournalWidget { pub struct JournalEntry { pub time: DateTime<Local>, pub level: LogLevel, + pub sub_system: String, pub msg: String, + pub registers: Option<[u32; 17]>, +} + +impl JournalEntry { + pub fn new(level: LogLevel, msg: String) -> Self { + let time = Local::now(); + let (msg, sub_system) = if msg.starts_with("[") { + if msg.find("]").is_some() { + let mut it = msg.strip_prefix("[").unwrap().split("]"); + let sub_system = it.next().unwrap().to_string(); + let msg = it.next().unwrap().to_string(); + (msg, sub_system) + } else { + (msg, String::new()) + } + } else { + (msg, String::new()) + }; + + Self { + time, + level, + sub_system, + msg, + registers: None, + } + } + + pub fn update_msg(&mut self, msg: String) { + let (msg, sub_system) = if msg.starts_with("[") { + if msg.find("]").is_some() { + let mut it = msg.strip_prefix("[").unwrap().split("]"); + let sub_system = it.next().unwrap().to_string(); + let msg = it.next().unwrap().to_string(); + (msg, sub_system) + } else { + (msg, String::new()) + } + } else { + (msg, String::new()) + }; + self.msg = msg; + self.sub_system = sub_system; + } } #[derive(Debug, Clone, Copy)] @@ -120,12 +304,12 @@ impl LogLevel { fn repr(self) -> Span<'static> { match self { LogLevel::Critical => "critical".red(), - LogLevel::Error => "error ".red(), - LogLevel::Warning => "warning ".yellow(), - LogLevel::Info => "info ".green(), - LogLevel::Debug => "debug ".magenta(), - LogLevel::Trace => "trace ".gray(), - LogLevel::Invalid => "invalid ".dark_gray(), + LogLevel::Error => "error ".red(), + LogLevel::Warning => "warning".yellow(), + LogLevel::Info => "info".green(), + LogLevel::Debug => "debug".magenta(), + LogLevel::Trace => "trace".gray(), + LogLevel::Invalid => "invalid".dark_gray(), } } @@ -141,3 +325,26 @@ impl LogLevel { } } } + +fn reg_name(r: usize) -> &'static str { + match r { + 0 => "r0", + 1 => "r1", + 2 => "r2", + 3 => "r3", + 4 => "r4", + 5 => "r5", + 6 => "r6", + 7 => "r7", + 8 => "r8", + 9 => "r9", + 10 => "r10", + 11 => "r11", + 12 => "r12", + 13 => "sp", + 14 => "lr", + 15 => "pc", + 16 => "psr", + _ => "inv", + } +} diff --git a/tools/src/sylveos/mod.rs b/tools/src/sylveos/mod.rs @@ -10,7 +10,6 @@ use std::{ time::Duration, }; -use chrono::Local; pub use console::ConsoleWidget; use crossterm::event::{Event, EventStream, KeyCode}; pub use journal::JournalWidget; @@ -116,6 +115,10 @@ impl App { let mut state = journal_state.write().unwrap(); state.append_last(msg); } + Message::Registers(regs) => { + let mut state = journal_state.write().unwrap(); + state.append_regs(regs); + } Message::Disconnect => { disconnected.store(true, Ordering::SeqCst); return; @@ -176,6 +179,12 @@ impl App { let disconnected = self.disconnected.load(Ordering::Relaxed); if let Some(key) = event.as_key_press_event() { match key.code { + KeyCode::Esc + if self.current_tab == TabIndex::Journal + && self.journal_widget.popup_shown() => + { + self.journal_widget.set_popup_shown(false); + } KeyCode::Esc => self.should_quit = true, KeyCode::Tab => self.current_tab = self.current_tab.next(), KeyCode::BackTab => self.current_tab = self.current_tab.previous(), @@ -208,6 +217,9 @@ impl App { KeyCode::Backspace if self.current_tab == TabIndex::Console && !disconnected => { self.console_widget.delete_char() } + KeyCode::Enter if self.current_tab == TabIndex::Journal => { + self.journal_widget.set_popup_shown(true); + } _ => {} } } @@ -221,6 +233,7 @@ enum Message { Console(u8), Journal(JournalEntry), Append(String), + Registers(Vec<u32>), Disconnect, } @@ -235,7 +248,6 @@ async fn parse_message(port: &mut ReadHalf<SerialStream>) -> Result<Message> { // next byte is level // next int is length of message // next bytes is message - let time = Local::now(); let level = journal::LogLevel::from_byte(port.read_u8().await?); let length = port.read_u32_le().await?; @@ -245,12 +257,11 @@ async fn parse_message(port: &mut ReadHalf<SerialStream>) -> Result<Message> { let msg = String::from_utf8_lossy(buffer.as_slice()).to_string(); - return Ok(Message::Journal(JournalEntry { time, level, msg })); + return Ok(Message::Journal(JournalEntry::new(level, msg))); } if byte == 2 { // Journal entry based off a sentinel - let time = Local::now(); let level = journal::LogLevel::from_byte(port.read_u8().await?); let mut buffer: Vec<u8> = Vec::new(); @@ -270,7 +281,7 @@ async fn parse_message(port: &mut ReadHalf<SerialStream>) -> Result<Message> { let msg = String::from_utf8_lossy(buffer.as_slice()).to_string(); - return Ok(Message::Journal(JournalEntry { time, level, msg })); + return Ok(Message::Journal(JournalEntry::new(level, msg))); } if byte == 3 { @@ -295,5 +306,19 @@ async fn parse_message(port: &mut ReadHalf<SerialStream>) -> Result<Message> { return Ok(Message::Append(msg)); } + if byte == 4 { + // Append to last entry registers + let mut buffer: Vec<u32> = Vec::with_capacity(17); + loop { + let n = port.read_u32_le().await?; + buffer.push(n); + if buffer.len() == 17 { + break; + } + } + + return Ok(Message::Registers(buffer)); + } + Ok(Message::Console(byte)) } diff --git a/tools/src/sylveos/paragraph_view.rs b/tools/src/sylveos/paragraph_view.rs @@ -12,9 +12,9 @@ use ratatui::{ #[derive(Debug, Clone)] pub struct ParagraphViewState { - vertical_scroll_state: ScrollbarState, - vertical_scroll: usize, - autoscroll: bool, + pub vertical_scroll_state: ScrollbarState, + pub vertical_scroll: usize, + pub autoscroll: bool, } impl Default for ParagraphViewState {