use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
#[derive(Debug, Clone, Default)]
pub struct InputState {
buffer: String,
cursor: usize,
original: Option<String>,
error: Option<String>,
pub context_path: Option<std::path::PathBuf>,
}
impl InputState {
pub fn new() -> Self {
Self::default()
}
pub fn with_initial(value: &str) -> Self {
Self {
buffer: value.to_string(),
cursor: value.len(),
original: Some(value.to_string()),
error: None,
context_path: None,
}
}
pub fn with_context(context_path: std::path::PathBuf) -> Self {
Self {
context_path: Some(context_path),
..Default::default()
}
}
pub fn buffer(&self) -> &str {
&self.buffer
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn original(&self) -> Option<&str> {
self.original.as_deref()
}
pub fn error(&self) -> Option<&str> {
self.error.as_deref()
}
pub fn set_error(&mut self, error: impl Into<String>) {
self.error = Some(error.into());
}
pub fn clear_error(&mut self) {
self.error = None;
}
pub fn handle_key(&mut self, key: KeyEvent) -> InputResult {
self.clear_error();
match (key.code, key.modifiers) {
(KeyCode::Enter, _) => {
let value = self.buffer.clone();
InputResult::Submit(value)
}
(KeyCode::Esc, _) => InputResult::Cancel,
(KeyCode::Backspace, _) => {
if self.cursor > 0 {
self.cursor -= 1;
self.buffer.remove(self.cursor);
}
InputResult::Continue
}
(KeyCode::Delete, _) => {
if self.cursor < self.buffer.len() {
self.buffer.remove(self.cursor);
}
InputResult::Continue
}
(KeyCode::Left, _) => {
self.cursor = self.cursor.saturating_sub(1);
InputResult::Continue
}
(KeyCode::Right, _) => {
self.cursor = (self.cursor + 1).min(self.buffer.len());
InputResult::Continue
}
(KeyCode::Home, _) | (KeyCode::Char('a'), KeyModifiers::CONTROL) => {
self.cursor = 0;
InputResult::Continue
}
(KeyCode::End, _) | (KeyCode::Char('e'), KeyModifiers::CONTROL) => {
self.cursor = self.buffer.len();
InputResult::Continue
}
(KeyCode::Char('u'), KeyModifiers::CONTROL) => {
self.buffer.clear();
self.cursor = 0;
InputResult::Continue
}
(KeyCode::Char('k'), KeyModifiers::CONTROL) => {
self.buffer.truncate(self.cursor);
InputResult::Continue
}
(KeyCode::Char('w'), KeyModifiers::CONTROL) => {
if self.cursor > 0 {
let before = &self.buffer[..self.cursor];
let word_start = before
.rfind(|c: char| c.is_whitespace())
.map(|i| i + 1)
.unwrap_or(0);
self.buffer.replace_range(word_start..self.cursor, "");
self.cursor = word_start;
}
InputResult::Continue
}
(KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => {
self.buffer.insert(self.cursor, c);
self.cursor += 1;
InputResult::Continue
}
_ => InputResult::Continue,
}
}
pub fn validate_filename(&self) -> Result<(), String> {
let name = &self.buffer;
if name.is_empty() {
return Err("Name cannot be empty".into());
}
if name.len() > 255 {
return Err("Name is too long (max 255 characters)".into());
}
if name.contains('/') {
return Err("Name cannot contain '/'".into());
}
if name.contains('\0') {
return Err("Name cannot contain null character".into());
}
if name == "." || name == ".." {
return Err("'.' and '..' are reserved names".into());
}
if name.starts_with(' ') || name.ends_with(' ') {
return Err("Name cannot start or end with spaces".into());
}
if name.ends_with('.') && name != "." {
return Err("Name cannot end with a dot".into());
}
Ok(())
}
pub fn has_changed(&self) -> bool {
self.original.as_deref() != Some(&self.buffer)
}
}
#[derive(Debug, Clone)]
pub enum InputResult {
Continue,
Cancel,
Submit(String),
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
fn key_event(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
KeyEvent {
code,
modifiers,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}
}
#[test]
fn test_input_basic() {
let mut input = InputState::new();
input.handle_key(key_event(KeyCode::Char('t'), KeyModifiers::NONE));
input.handle_key(key_event(KeyCode::Char('e'), KeyModifiers::NONE));
input.handle_key(key_event(KeyCode::Char('s'), KeyModifiers::NONE));
input.handle_key(key_event(KeyCode::Char('t'), KeyModifiers::NONE));
assert_eq!(input.buffer(), "test");
assert_eq!(input.cursor(), 4);
}
#[test]
fn test_input_backspace() {
let mut input = InputState::with_initial("test");
input.handle_key(key_event(KeyCode::Backspace, KeyModifiers::NONE));
assert_eq!(input.buffer(), "tes");
assert_eq!(input.cursor(), 3);
}
#[test]
fn test_input_cursor_movement() {
let mut input = InputState::with_initial("test");
input.handle_key(key_event(KeyCode::Home, KeyModifiers::NONE));
assert_eq!(input.cursor(), 0);
input.handle_key(key_event(KeyCode::End, KeyModifiers::NONE));
assert_eq!(input.cursor(), 4);
input.handle_key(key_event(KeyCode::Left, KeyModifiers::NONE));
assert_eq!(input.cursor(), 3);
input.handle_key(key_event(KeyCode::Right, KeyModifiers::NONE));
assert_eq!(input.cursor(), 4);
}
#[test]
fn test_validate_filename() {
let mut input = InputState::new();
input.buffer = "test.txt".to_string();
assert!(input.validate_filename().is_ok());
input.buffer = ".hidden".to_string();
assert!(input.validate_filename().is_ok());
input.buffer = "my-file_v2".to_string();
assert!(input.validate_filename().is_ok());
input.buffer = "".to_string();
assert!(input.validate_filename().is_err());
input.buffer = "test/file".to_string();
assert!(input.validate_filename().is_err());
input.buffer = ".".to_string();
assert!(input.validate_filename().is_err());
input.buffer = "..".to_string();
assert!(input.validate_filename().is_err());
}
#[test]
fn test_submit_and_cancel() {
let mut input = InputState::with_initial("test");
let result = input.handle_key(key_event(KeyCode::Enter, KeyModifiers::NONE));
assert!(matches!(result, InputResult::Submit(s) if s == "test"));
let result = input.handle_key(key_event(KeyCode::Esc, KeyModifiers::NONE));
assert!(matches!(result, InputResult::Cancel));
}
}