pub struct PromptEditor { bytes: [u8; N], length: usize, cursor: usize, } impl PromptEditor { 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 { entries: [[u8; N]; H], lengths: [usize; H], count: usize, draft: [u8; N], draft_length: usize, } impl PromptHistory { 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) -> 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) { 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); } }