Files
furumi_tui/src/app/input.rs
T
2026-07-17 01:17:54 +03:00

148 lines
4.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! One-line text editing with a movable cursor, shared by every text input
//! (command line, edit forms, popup fields).
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
/// A single-line value plus a cursor position (in characters). Dereferences
/// to `&str`, so read paths treat it like the plain string it wraps.
#[derive(Debug, Clone, Default)]
pub struct LineEdit {
value: String,
cursor: usize,
}
impl LineEdit {
/// Starts with `value` and the cursor at its end.
pub fn new(value: impl Into<String>) -> Self {
let value = value.into();
let cursor = value.chars().count();
Self { value, cursor }
}
pub fn as_str(&self) -> &str {
&self.value
}
/// Cursor position in characters (0..=len).
pub fn cursor(&self) -> usize {
self.cursor
}
fn byte_index(&self, chars: usize) -> usize {
self.value
.char_indices()
.nth(chars)
.map(|(index, _)| index)
.unwrap_or(self.value.len())
}
pub fn clear(&mut self) {
self.value.clear();
self.cursor = 0;
}
pub fn insert(&mut self, c: char) {
let at = self.byte_index(self.cursor);
self.value.insert(at, c);
self.cursor += 1;
}
pub fn insert_str(&mut self, s: &str) {
let at = self.byte_index(self.cursor);
self.value.insert_str(at, s);
self.cursor += s.chars().count();
}
/// Removes the character before the cursor; `false` when at the start.
pub fn backspace(&mut self) -> bool {
if self.cursor == 0 {
return false;
}
let at = self.byte_index(self.cursor - 1);
self.value.remove(at);
self.cursor -= 1;
true
}
/// Removes the character under the cursor.
pub fn delete(&mut self) {
if self.cursor < self.value.chars().count() {
let at = self.byte_index(self.cursor);
self.value.remove(at);
}
}
/// Applies one editing key (characters, backspace/delete, cursor
/// movement). Returns `false` for keys this editor does not handle
/// (Enter, Esc, Tab, ...), which the caller interprets itself.
pub fn handle_key(&mut self, key: KeyEvent) -> bool {
match key.code {
KeyCode::Char(c) if key.modifiers.difference(KeyModifiers::SHIFT).is_empty() => {
self.insert(c);
}
KeyCode::Backspace => {
self.backspace();
}
KeyCode::Delete => self.delete(),
KeyCode::Left => self.cursor = self.cursor.saturating_sub(1),
KeyCode::Right => self.cursor = (self.cursor + 1).min(self.value.chars().count()),
KeyCode::Home => self.cursor = 0,
KeyCode::End => self.cursor = self.value.chars().count(),
_ => return false,
}
true
}
}
impl std::ops::Deref for LineEdit {
type Target = str;
fn deref(&self) -> &str {
&self.value
}
}
impl std::fmt::Display for LineEdit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.value)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::KeyEvent;
fn key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
#[test]
fn edits_at_cursor() {
let mut edit = LineEdit::new("hllo");
assert_eq!(edit.cursor(), 4);
edit.handle_key(key(KeyCode::Home));
edit.handle_key(key(KeyCode::Right));
edit.handle_key(key(KeyCode::Char('e')));
assert_eq!(edit.as_str(), "hello");
assert_eq!(edit.cursor(), 2);
edit.handle_key(key(KeyCode::End));
edit.handle_key(key(KeyCode::Backspace));
assert_eq!(edit.as_str(), "hell");
edit.handle_key(key(KeyCode::Home));
edit.handle_key(key(KeyCode::Delete));
assert_eq!(edit.as_str(), "ell");
}
#[test]
fn multibyte_safe() {
let mut edit = LineEdit::new("метл");
edit.handle_key(key(KeyCode::Left));
edit.insert('а');
assert_eq!(edit.as_str(), "метал");
edit.handle_key(key(KeyCode::End));
edit.insert('л');
assert_eq!(edit.as_str(), "металл");
}
}