#[derive(Debug, Clone, Default)]
pub struct PaletteState {
buffer: String,
cursor: usize,
}
impl PaletteState {
pub fn buffer(&self) -> &str {
&self.buffer
}
pub fn is_empty(&self) -> bool {
self.buffer.is_empty()
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn push_char(&mut self, c: char) {
self.buffer.push(c);
self.cursor = 0;
}
const MAX_BUFFER_BYTES: usize = 64 * 1024;
pub fn push_str(&mut self, s: &str) -> bool {
let available = Self::MAX_BUFFER_BYTES.saturating_sub(self.buffer.len());
let to_take = available.min(s.len());
let mut cut = to_take;
while cut > 0 && !s.is_char_boundary(cut) {
cut -= 1;
}
self.buffer.push_str(&s[..cut]);
self.cursor = 0;
cut < s.len()
}
pub fn pop_char(&mut self) -> Option<char> {
let popped = self.buffer.pop();
self.cursor = 0;
popped
}
pub fn word_delete(&mut self) {
while self.buffer.chars().last().is_some_and(char::is_whitespace) {
self.buffer.pop();
}
while self
.buffer
.chars()
.last()
.is_some_and(|c| !c.is_whitespace())
{
self.buffer.pop();
}
self.cursor = 0;
}
pub fn clear(&mut self) {
self.buffer.clear();
self.cursor = 0;
}
pub fn advance_cursor(&mut self, filtered_len: usize) {
if let Some(max_idx) = filtered_len.checked_sub(1) {
self.cursor = (self.cursor + 1).min(max_idx);
}
}
pub fn retreat_cursor(&mut self) {
self.cursor = self.cursor.saturating_sub(1);
}
pub fn set_buffer(&mut self, s: String) {
self.buffer = s;
self.cursor = 0;
}
pub fn take_buffer(&mut self) -> String {
let out = std::mem::take(&mut self.buffer);
self.cursor = 0;
out
}
}
#[cfg(test)]
mod tests {
use super::PaletteState;
#[test]
fn push_resets_cursor_and_appends() {
let mut p = PaletteState::default();
p.advance_cursor(5);
assert_ne!(p.cursor(), 0, "precondition: cursor seeded");
p.push_char('h');
assert_eq!(p.cursor(), 0);
assert_eq!(p.buffer(), "h");
}
#[test]
fn pop_returns_char_and_resets_cursor() {
let mut p = PaletteState::default();
p.push_char('a');
p.push_char('b');
p.advance_cursor(3);
assert_ne!(p.cursor(), 0);
let popped = p.pop_char();
assert_eq!(popped, Some('b'));
assert_eq!(p.buffer(), "a");
assert_eq!(p.cursor(), 0);
}
#[test]
fn pop_empty_returns_none() {
let mut p = PaletteState::default();
assert!(p.pop_char().is_none());
}
#[test]
fn word_delete_peels_whitespace_then_word() {
let mut p = PaletteState::default();
"hello world ".chars().for_each(|c| p.push_char(c));
p.advance_cursor(4);
p.word_delete();
assert_eq!(p.buffer(), "hello ");
assert_eq!(p.cursor(), 0);
}
#[test]
fn clear_resets_buffer_and_cursor() {
let mut p = PaletteState::default();
p.push_char('x');
p.advance_cursor(9);
p.clear();
assert_eq!(p.buffer(), "");
assert_eq!(p.cursor(), 0);
}
#[test]
fn advance_cursor_caps_at_max() {
let mut p = PaletteState::default();
for _ in 0..10 {
p.advance_cursor(4);
}
assert_eq!(p.cursor(), 3);
}
#[test]
fn advance_cursor_with_empty_list_is_noop() {
let mut p = PaletteState::default();
p.advance_cursor(0);
assert_eq!(p.cursor(), 0);
}
#[test]
fn retreat_saturates_at_zero() {
let mut p = PaletteState::default();
p.retreat_cursor();
assert_eq!(p.cursor(), 0);
}
#[test]
fn set_buffer_overwrites_and_resets_cursor() {
let mut p = PaletteState::default();
p.advance_cursor(7);
p.set_buffer("diff".to_string());
assert_eq!(p.buffer(), "diff");
assert_eq!(p.cursor(), 0);
}
#[test]
fn take_buffer_returns_prior_and_empties() {
let mut p = PaletteState::default();
p.push_char('q');
p.push_char('!');
let taken = p.take_buffer();
assert_eq!(taken, "q!");
assert_eq!(p.buffer(), "");
assert_eq!(p.cursor(), 0);
}
}