commit 4e216bc7abed8d7f0143c87bbb4dc8a8d39dcbb2
parent da41585bef676857b6810f6ae680dd30519175d2
Author: Sylvia Ivory <git@sivory.net>
Date: Mon, 16 Mar 2026 14:24:44 -0700
Add register view
Diffstat:
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(®s);
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 {