Something went wrong. Try again.
Codex, Claude, and local AI agents on the Commodore 64 Ultimate.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493use 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 }}