use core::ptr::write_volatile; use crate::{ dashboard::{Dashboard, DashboardRows, PAGE_ROWS, TAB_BAR}, glyph::screen_code, scrollback::{ScrollbackView, VIEW_ROWS}, session::SessionList, transcript::{Transcript, TranscriptStorage, TRANSCRIPT_COLUMNS}, uci, }; const SCREEN_RAM: *mut u8 = 0xa400 as *mut u8; const COLOR_RAM: *mut u8 = 0xd800 as *mut u8; const SCREEN_WIDTH: usize = 40; const SCREEN_HEIGHT: usize = 25; const SCREEN_SIZE: usize = SCREEN_WIDTH * SCREEN_HEIGHT; const TRANSCRIPT_FIRST_ROW: usize = 0; const TRANSCRIPT_LAST_ROW: usize = 20; const STATUS_ROW: usize = 21; const INPUT_FIRST_ROW: usize = 22; const INPUT_LAST_ROW: usize = 24; pub const WHITE: u8 = 1; pub const CYAN: u8 = 3; pub const YELLOW: u8 = 7; const LIGHT_RED: u8 = 10; pub const LIGHT_GREEN: u8 = 13; #[link_section = ".transcript"] static mut TRANSCRIPT_STORAGE: TranscriptStorage = TranscriptStorage::uninitialized(); pub struct Screen { transcript: Transcript, } impl Screen { pub const fn new() -> Self { Self { transcript: Transcript::new(), } } pub fn clear(&mut self) { let mut offset = 0usize; while offset < SCREEN_SIZE { unsafe { write_volatile(SCREEN_RAM.add(offset), b' '); write_volatile(COLOR_RAM.add(offset), WHITE); } offset += 1; } } pub fn setup_terminal(&mut self) { self.clear(); unsafe { self.transcript .initialize(&mut *core::ptr::addr_of_mut!(TRANSCRIPT_STORAGE), WHITE); } self.restore_composer(&[], 0); } pub fn render_connection_settings( &mut self, host: &[u8], host_cursor: usize, workspace: &[u8], workspace_cursor: usize, access: &[u8], field: u8, status: &[u8], ) { self.write_color(0, 0, b"CONNECTION", CYAN); self.write_color(1, 0, b"EDIT THEN RETURN", WHITE); let host_color = if field == 0 { YELLOW } else { CYAN }; self.write_color(3, 0, b"BRIDGE IPV4", host_color); self.render_editable(4, 1, host, host_cursor, field == 0); let workspace_color = if field == 1 { YELLOW } else { CYAN }; self.write_color(6, 0, b"WORKSPACE", workspace_color); self.render_editable(7, 3, workspace, workspace_cursor, field == 1); self.write_color(11, 0, b"UNDER ALLOWED ROOT", CYAN); let access_color = if field == 2 { YELLOW } else { CYAN }; self.write_color(13, 0, b"ACCESS MODE", access_color); self.write_padded(14, 1, access, access_color, SCREEN_WIDTH - 1); self.write_padded(20, 0, status, LIGHT_RED, SCREEN_WIDTH); } pub fn render_connecting(&mut self, host: &[u8], workspace: &[u8]) { self.clear(); self.write_color(0, 0, b"CONNECTING", CYAN); self.write_color(2, 0, b"BRIDGE:", WHITE); self.write_color(2, 8, host, YELLOW); self.write_color(4, 0, b"WORKSPACE:", WHITE); self.write_color(5, 0, workspace, YELLOW); self.write_color(8, 0, b"OPENING TCP...", WHITE); } pub fn render_session_picker( &mut self, sessions: &SessionList, selected: usize, new_workspace: &[u8], ) { self.clear(); self.write_color(0, 0, b"AGENT SESSION", CYAN); let new_color = if selected == 0 { YELLOW } else { WHITE }; self.write_color(2, 0, if selected == 0 { b">" } else { b" " }, new_color); self.write_color(2, 2, b"NEW SESSION", new_color); let mut index = 0usize; while index < sessions.count() { let row = 3 + index; let active = selected == index + 1; let color = if active { YELLOW } else { WHITE }; self.write_color(row, 0, if active { b">" } else { b" " }, color); if let Some(label) = sessions.label(index) { self.write_color(row, 2, label, color); } index += 1; } self.write_color(13, 0, b"NEW WORKSPACE", CYAN); let mut offset = 0usize; let mut row = 14usize; while offset < new_workspace.len() && row < 17 { let end = (offset + SCREEN_WIDTH).min(new_workspace.len()); self.write_color(row, 0, &new_workspace[offset..end], WHITE); offset = end; row += 1; } } pub fn write(&mut self, row: usize, column: usize, text: &[u8]) { self.write_color(row, column, text, WHITE); } pub fn set_status(&mut self, text: &[u8]) { self.clear_row(STATUS_ROW); self.write_color(STATUS_ROW, 0, text, YELLOW); } pub fn set_reasoning_status(&mut self, mode: &[u8]) { self.clear_row(STATUS_ROW); self.write_color(STATUS_ROW, 0, b"REASONING: ", YELLOW); self.write_color(STATUS_ROW, 11, mode, YELLOW); } pub fn restore_composer(&mut self, text: &[u8], cursor: usize) { self.set_status(b"READY - TYPE A PROMPT"); self.render_input(text, cursor); } pub fn render_settings(&mut self, mode: &[u8]) { let mut row = STATUS_ROW; while row < SCREEN_HEIGHT { self.clear_row(row); row += 1; } self.write_color(21, 0, b"SETTINGS", CYAN); self.write_color(22, 0, b"REASONING: ", WHITE); self.write_color(22, 11, mode, YELLOW); } pub fn render_approval_controls(&mut self, kind: &[u8]) { self.clear_row(STATUS_ROW); self.clear_row(22); self.clear_row(23); self.clear_row(24); self.write_color(STATUS_ROW, 0, b"APPROVAL: ", YELLOW); self.write_color(STATUS_ROW, 10, kind, YELLOW); } pub fn render_dashboard(&mut self, dashboard: &Dashboard, rows: &DashboardRows) { self.transcript.set_visible(false); self.clear(); self.write_color(0, 0, TAB_BAR, CYAN); let (tab_start, tab_end) = dashboard.tab.span(); self.write_color(0, tab_start, &TAB_BAR[tab_start..tab_end], YELLOW); let first = dashboard.page_start(); let mut offset = 0usize; while offset < PAGE_ROWS { let index = first + offset; if index >= dashboard.count() { break; } if let Some(text) = rows.row(index) { let selected = index == dashboard.selected(); let kind = rows.kind(index).unwrap_or(0); let text_color = if dashboard.is_diff() { match kind { 3 => CYAN, _ => WHITE, } } else if dashboard.tab == crate::dashboard::DashboardTab::Activity { match kind { 0 => YELLOW, 1 => LIGHT_GREEN, 2 => LIGHT_RED, _ => WHITE, } } else if selected { YELLOW } else { WHITE }; self.write_color( 2 + offset, 0, if selected { b">" } else { b" " }, if selected { YELLOW } else { WHITE }, ); self.write_color(2 + offset, 2, &text[..text.len().min(38)], text_color); } offset += 1; } } pub fn begin_scrollback(&mut self) -> ScrollbackView { self.transcript.set_visible(false); let view = ScrollbackView::new(self.transcript.count()); self.render_scrollback(&view); view } pub fn render_scrollback(&mut self, view: &ScrollbackView) { let mut row = TRANSCRIPT_FIRST_ROW; while row <= TRANSCRIPT_LAST_ROW { self.clear_row(row); row += 1; } let mut line = view.start(); let mut output_row = TRANSCRIPT_FIRST_ROW; while line < view.end() { self.render_scrollback_line(line, output_row); line += 1; output_row += 1; } self.clear_row(STATUS_ROW); self.write_color(STATUS_ROW, 0, b"SCROLL ", YELLOW); let mut column = self.write_decimal(STATUS_ROW, 7, view.start() + 1, YELLOW); self.write_color(STATUS_ROW, column, b"-", YELLOW); column += 1; column = self.write_decimal(STATUS_ROW, column, view.end(), YELLOW); self.write_color(STATUS_ROW, column, b"/", YELLOW); column += 1; self.write_decimal(STATUS_ROW, column, view.total(), YELLOW); self.clear_row(22); self.clear_row(23); self.clear_row(24); } pub fn show_clear_confirmation(&mut self) { self.clear_row(STATUS_ROW); self.clear_row(22); self.clear_row(23); self.clear_row(24); self.write_color(STATUS_ROW, 0, b"CLEAR TRANSCRIPT?", YELLOW); } pub fn restore_live_transcript(&mut self) { self.transcript.set_visible(true); self.render_live_transcript(); } pub fn clear_transcript(&mut self) { unsafe { self.transcript .clear(&mut *core::ptr::addr_of_mut!(TRANSCRIPT_STORAGE), WHITE); } self.render_live_transcript(); } pub fn render_input(&mut self, text: &[u8], cursor: usize) { self.write_color(INPUT_FIRST_ROW, 0, b"> ", CYAN); self.render_editable_at( INPUT_FIRST_ROW * SCREEN_WIDTH + 2, (INPUT_LAST_ROW - INPUT_FIRST_ROW + 1) * SCREEN_WIDTH - 2, text, cursor, true, ); } pub fn transcript_speaker(&mut self, label: &[u8], color: u8) { unsafe { self.transcript.speaker( &mut *core::ptr::addr_of_mut!(TRANSCRIPT_STORAGE), label, color, ); } self.refresh_transcript_if_visible(); } pub fn transcript_write(&mut self, text: &[u8]) { self.transcript_write_color(text, WHITE); } pub fn transcript_newline(&mut self) { unsafe { self.transcript .newline(&mut *core::ptr::addr_of_mut!(TRANSCRIPT_STORAGE), WHITE); } self.refresh_transcript_if_visible(); } fn transcript_write_color(&mut self, text: &[u8], color: u8) { unsafe { self.transcript.write( &mut *core::ptr::addr_of_mut!(TRANSCRIPT_STORAGE), text, color, ); } self.refresh_transcript_if_visible(); } pub fn error(&mut self, text: &[u8]) { self.write_color(21, 0, text, LIGHT_RED); } pub fn uci_error(&mut self, label: &[u8], error: uci::Error) { self.write_color(21, 0, label, LIGHT_RED); self.write_color(22, 0, error.message(), LIGHT_RED); if let Some(status) = error.status_code() { self.write_color(23, 0, b"STATUS: $", LIGHT_RED); self.write_hex(23, 9, status, LIGHT_RED); } } fn render_scrollback_line(&mut self, line: usize, output_row: usize) { unsafe { if let Some((characters, colors)) = self .transcript .line(&*core::ptr::addr_of!(TRANSCRIPT_STORAGE), line) { let mut column = 0usize; while column < TRANSCRIPT_COLUMNS { let screen = output_row * SCREEN_WIDTH + column; write_volatile(SCREEN_RAM.add(screen), screen_code(characters[column])); write_volatile(COLOR_RAM.add(screen), colors[column]); column += 1; } } } } fn refresh_transcript_if_visible(&mut self) { if self.transcript.is_visible() { self.render_live_transcript(); } } fn render_live_transcript(&mut self) { let first = self.transcript.count().saturating_sub(VIEW_ROWS); let mut output_row = TRANSCRIPT_FIRST_ROW; while output_row <= TRANSCRIPT_LAST_ROW { let line = first + output_row; if line < self.transcript.count() { self.render_scrollback_line(line, output_row); } else { self.clear_row(output_row); } output_row += 1; } } fn fill_row(&mut self, row: usize, byte: u8, color: u8) { let start = row * SCREEN_WIDTH; let mut column = 0usize; while column < SCREEN_WIDTH { self.write_offset(start + column, byte, color, false); column += 1; } } fn clear_row(&mut self, row: usize) { self.fill_row(row, b' ', WHITE); } fn render_editable( &mut self, first_row: usize, rows: usize, text: &[u8], cursor: usize, active: bool, ) { self.render_editable_at( first_row * SCREEN_WIDTH, rows * SCREEN_WIDTH, text, cursor, active, ); } fn render_editable_at( &mut self, start: usize, capacity: usize, text: &[u8], cursor: usize, active: bool, ) { let length = text.len().min(capacity); let mut index = 0usize; while index < length { self.write_offset(start + index, text[index], WHITE, false); index += 1; } while index < capacity { self.write_offset(start + index, b' ', WHITE, false); index += 1; } let cursor = cursor.min(text.len()); if active && cursor < capacity { let (byte, reverse) = if cursor < text.len() { (text[cursor], true) } else { (b'_', false) }; self.write_offset(start + cursor, byte, CYAN, reverse); } } fn write_padded(&mut self, row: usize, column: usize, text: &[u8], color: u8, width: usize) { let start = row * SCREEN_WIDTH + column; let mut index = 0usize; while index < width { let byte = if index < text.len() { text[index] } else { b' ' }; self.write_offset(start + index, byte, color, false); index += 1; } } fn write_color(&mut self, row: usize, column: usize, text: &[u8], color: u8) { if row >= SCREEN_HEIGHT || column >= SCREEN_WIDTH { return; } let start = row * SCREEN_WIDTH + column; let length = text.len().min(SCREEN_WIDTH - column); let mut index = 0usize; while index < length { self.write_offset(start + index, text[index], color, false); index += 1; } } fn write_byte(&mut self, row: usize, column: usize, byte: u8, color: u8) { let offset = row * SCREEN_WIDTH + column; self.write_offset(offset, byte, color, false); } fn write_offset(&mut self, offset: usize, byte: u8, color: u8, reverse: bool) { unsafe { let reverse_bit = if reverse { 0x80 } else { 0 }; write_volatile(SCREEN_RAM.add(offset), screen_code(byte) | reverse_bit); write_volatile(COLOR_RAM.add(offset), color); } } fn write_hex(&mut self, row: usize, column: usize, value: u8, color: u8) { const HEX: &[u8; 16] = b"0123456789ABCDEF"; let text = [HEX[(value >> 4) as usize], HEX[(value & 0x0f) as usize]]; self.write_color(row, column, &text, color); } fn write_decimal(&mut self, row: usize, column: usize, value: usize, color: u8) -> usize { let mut current = column; if value >= 100 { self.write_byte(row, current, b'0' + ((value / 100) % 10) as u8, color); current += 1; } if value >= 10 { self.write_byte(row, current, b'0' + ((value / 10) % 10) as u8, color); current += 1; } self.write_byte(row, current, b'0' + (value % 10) as u8, color); current + 1 } }