use ratatui::text::{ Line, Span };
use unicode_width::UnicodeWidthChar;
pub struct InputBuffer {
chars: Vec<char>,
cursor: usize,
history: Vec<String>,
history_pos: usize,
stash: Option<String>, prefix: Option<String>, }
impl Default for InputBuffer
{
fn default() -> Self { Self::new() }
}
impl InputBuffer
{
pub fn new() -> Self
{
Self { chars: Vec::new(), cursor: 0, history: Vec::new(), history_pos: 0, stash: None, prefix: None }
}
pub fn is_empty(&self) -> bool { self.chars.is_empty() }
pub fn text(&self) -> String { self.chars.iter().collect() }
pub fn cursor(&self) -> usize { self.cursor }
pub fn insert(&mut self, c: char)
{
self.chars.insert(self.cursor, c);
self.cursor += 1;
}
pub fn insert_str(&mut self, text: &str)
{
for c in text.chars() { self.insert(c); }
}
pub fn backspace(&mut self)
{
if self.cursor > 0
{
self.cursor -= 1;
self.chars.remove(self.cursor);
}
}
pub fn delete(&mut self)
{
if self.cursor < self.chars.len() { self.chars.remove(self.cursor); }
}
pub fn delete_word(&mut self) {
let start = self.word_start();
self.chars.drain(start..self.cursor);
self.cursor = start;
}
pub fn kill_to_start(&mut self) {
let start = self.line_start();
self.chars.drain(start..self.cursor);
self.cursor = start;
}
pub fn kill_to_end(&mut self) {
let end = self.line_end();
self.chars.drain(self.cursor..end);
}
pub fn left(&mut self) { self.cursor = self.cursor.saturating_sub(1); }
pub fn right(&mut self) { if self.cursor < self.chars.len() { self.cursor += 1; } }
pub fn home(&mut self) { self.cursor = self.line_start(); }
pub fn end(&mut self) { self.cursor = self.line_end(); }
pub fn word_left(&mut self) { self.cursor = self.word_start(); }
pub fn word_right(&mut self)
{
let len = self.chars.len();
let mut i = self.cursor;
while i < len && self.chars[i].is_whitespace() { i += 1; }
while i < len && !self.chars[i].is_whitespace() { i += 1; }
self.cursor = i;
}
pub fn history_up(&mut self)
{
if self.history.is_empty() || self.history_pos == 0 { return; }
if self.history_pos == self.history.len()
{
let typed = self.text();
self.prefix = (!typed.is_empty() && !typed.starts_with('/')).then(|| typed.clone());
self.stash = Some(typed);
}
let Some(found) = self.history[..self.history_pos].iter().rposition(|e| self.matches(e))
else { return; };
self.history_pos = found;
self.set(&self.history[found].clone());
}
pub fn history_down(&mut self)
{
if self.history_pos >= self.history.len() { return; }
let next = self.history.iter().enumerate().skip(self.history_pos + 1)
.find(|(_, e)| self.matches(e)).map(|(i, _)| i);
let new = match next
{
Some(i) =>
{
self.history_pos = i;
self.history[i].clone()
},
None => {
self.history_pos = self.history.len();
self.prefix = None;
self.stash.take().unwrap_or_default()
}
};
self.set(&new);
}
pub fn push_history(&mut self, input: &str)
{
if self.history.last().map(String::as_str) != Some(input)
{
self.history.push(input.to_owned());
}
self.history_pos = self.history.len();
self.stash = None;
self.prefix = None;
}
pub fn reset_history_position(&mut self)
{
self.history_pos = self.history.len();
self.stash = None;
self.prefix = None;
}
pub fn clear(&mut self)
{
self.chars.clear();
self.cursor = 0;
}
pub fn take(&mut self) -> String {
let read = self.text();
self.clear();
self.reset_history_position();
read
}
pub fn render(&self, width: u16, mask: bool) -> (Vec<Line<'static>>, (u16, u16))
{
let width = width.max(1) as usize;
let mut lines: Vec<Line<'static>> = Vec::new();
let mut current = String::new();
let mut column = 0usize;
let mut cursor_at = (0u16, 0u16);
for (i, c) in self.chars.iter().enumerate()
{
if i == self.cursor { cursor_at = (column as u16, lines.len() as u16); }
if *c == '\n'
{
lines.push(Line::from(Span::raw(std::mem::take(&mut current))));
column = 0;
continue;
}
let shown = if mask { '*' } else { *c };
let w = shown.width().unwrap_or(0);
if column + w > width
{
lines.push(Line::from(Span::raw(std::mem::take(&mut current))));
column = 0;
if i == self.cursor { cursor_at = (0, lines.len() as u16); }
}
current.push(shown);
column += w;
}
if self.cursor == self.chars.len() { cursor_at = (column as u16, lines.len() as u16); }
lines.push(Line::from(Span::raw(current)));
(lines, cursor_at)
}
fn matches(&self, entry: &str) -> bool {
self.prefix.as_ref().is_none_or(|p| entry.starts_with(p.as_str()))
}
fn set(&mut self, text: &str)
{
self.chars = text.chars().collect();
self.cursor = self.chars.len();
}
fn line_start(&self) -> usize
{
self.chars[..self.cursor].iter().rposition(|c| *c == '\n').map(|i| i + 1).unwrap_or(0)
}
fn line_end(&self) -> usize
{
self.chars[self.cursor..].iter().position(|c| *c == '\n')
.map(|i| self.cursor + i).unwrap_or(self.chars.len())
}
fn word_start(&self) -> usize
{
let mut i = self.cursor;
while i > 0 && self.chars[i - 1].is_whitespace() { i -= 1; }
while i > 0 && !self.chars[i - 1].is_whitespace() { i -= 1; }
i
}
}