Something went wrong. Try again.
Codex, Claude, and local AI agents on the Commodore 64 Ultimate.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253pub struct PromptEditor<const N: usize> { bytes: [u8; N], length: usize, cursor: usize,}
impl<const N: usize> PromptEditor<N> { pub const fn new() -> Self { Self { bytes: [0; N], length: 0, cursor: 0, } }
pub fn as_slice(&self) -> &[u8] { &self.bytes[..self.length] }
pub const fn length(&self) -> usize { self.length }
pub const fn cursor(&self) -> usize { self.cursor }
pub fn clear(&mut self) { self.length = 0; self.cursor = 0; }
pub fn set_text(&mut self, text: &[u8]) { set_text(&mut self.bytes, &mut self.length, &mut self.cursor, text); }
pub fn move_home(&mut self) { self.cursor = 0; }
pub fn move_left(&mut self) { if self.cursor != 0 { self.cursor -= 1; } }
pub fn move_right(&mut self) { if self.cursor < self.length { self.cursor += 1; } }
pub fn insert(&mut self, byte: u8) -> bool { insert(&mut self.bytes, &mut self.length, &mut self.cursor, byte) }
pub fn backspace(&mut self) -> bool { backspace(&mut self.bytes, &mut self.length, &mut self.cursor) }}
#[inline(never)]fn set_text(bytes: &mut [u8], length: &mut usize, cursor: &mut usize, text: &[u8]) { let new_length = text.len().min(bytes.len()); bytes[..new_length].copy_from_slice(&text[..new_length]); *length = new_length; *cursor = new_length;}
#[inline(never)]fn insert(bytes: &mut [u8], length: &mut usize, cursor: &mut usize, byte: u8) -> bool { if *length == bytes.len() { return false; } unsafe { core::ptr::copy( bytes.as_ptr().add(*cursor), bytes.as_mut_ptr().add(*cursor + 1), *length - *cursor, ); } bytes[*cursor] = byte; *cursor += 1; *length += 1; true}
#[inline(never)]fn backspace(bytes: &mut [u8], length: &mut usize, cursor: &mut usize) -> bool { if *cursor == 0 { return false; } let removed = *cursor - 1; unsafe { core::ptr::copy( bytes.as_ptr().add(removed + 1), bytes.as_mut_ptr().add(removed), *length - removed - 1, ); } *cursor = removed; *length -= 1; true}
pub struct PromptHistory<const N: usize, const H: usize> { entries: [[u8; N]; H], lengths: [usize; H], count: usize, draft: [u8; N], draft_length: usize,}
impl<const N: usize, const H: usize> PromptHistory<N, H> { pub const fn new() -> Self { Self { entries: [[0; N]; H], lengths: [0; H], count: 0, draft: [0; N], draft_length: 0, } }
pub const fn count(&self) -> usize { self.count }
pub fn remember(&mut self, text: &[u8]) { if H == 0 || text.is_empty() { return; }
let length = text.len().min(N); if self.count != 0 { let latest = self.count - 1; if self.lengths[latest] == length && self.entries[latest][..length] == text[..length] { return; } }
let destination = if self.count < H { let index = self.count; self.count += 1; index } else { unsafe { core::ptr::copy( self.entries.as_ptr().add(1), self.entries.as_mut_ptr(), H - 1, ); core::ptr::copy( self.lengths.as_ptr().add(1), self.lengths.as_mut_ptr(), H - 1, ); } H - 1 };
self.entries[destination][..length].copy_from_slice(&text[..length]); self.lengths[destination] = length; }
pub fn save_draft(&mut self, text: &[u8]) { let length = text.len().min(N); self.draft[..length].copy_from_slice(&text[..length]); self.draft_length = length; }
pub fn load(&self, index: usize, editor: &mut PromptEditor<N>) -> bool { if index >= self.count { return false; } editor.set_text(&self.entries[index][..self.lengths[index]]); true }
pub fn load_draft(&self, editor: &mut PromptEditor<N>) { editor.set_text(&self.draft[..self.draft_length]); }}
#[cfg(test)]mod tests { use super::{PromptEditor, PromptHistory};
#[test] fn inserts_and_backspaces_at_cursor() { let mut editor = PromptEditor::<8>::new(); for byte in b"AC" { assert!(editor.insert(*byte)); } editor.move_left(); assert!(editor.insert(b'B')); assert_eq!(editor.as_slice(), b"ABC"); assert_eq!(editor.cursor(), 2);
assert!(editor.backspace()); assert_eq!(editor.as_slice(), b"AC"); assert_eq!(editor.cursor(), 1); }
#[test] fn cursor_stays_within_text() { let mut editor = PromptEditor::<4>::new(); editor.move_left(); assert_eq!(editor.cursor(), 0); assert!(editor.insert(b'A')); editor.move_right(); assert_eq!(editor.cursor(), 1); editor.move_home(); assert_eq!(editor.cursor(), 0); editor.clear(); assert_eq!(editor.as_slice(), b""); }
#[test] fn sets_text_and_truncates_to_capacity() { let mut editor = PromptEditor::<4>::new(); editor.set_text(b"HELLO");
assert_eq!(editor.as_slice(), b"HELL"); assert_eq!(editor.cursor(), 4); }
#[test] fn history_drops_oldest_and_restores_draft() { let mut history = PromptHistory::<8, 2>::new(); history.remember(b"ONE"); history.remember(b"TWO"); history.remember(b"THREE"); history.save_draft(b"DRAFT");
let mut editor = PromptEditor::<8>::new(); assert!(history.load(0, &mut editor)); assert_eq!(editor.as_slice(), b"TWO"); assert!(history.load(1, &mut editor)); assert_eq!(editor.as_slice(), b"THREE"); history.load_draft(&mut editor); assert_eq!(editor.as_slice(), b"DRAFT"); }
#[test] fn history_skips_consecutive_duplicates() { let mut history = PromptHistory::<8, 4>::new(); history.remember(b"SAME"); history.remember(b"SAME"); assert_eq!(history.count(), 1); }}