use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
pub use handlers::*;
pub use mode::Mode;
use std::{fmt::Display, ops::RangeBounds};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
mod handlers;
mod mode;
enum InputChange {
Insert,
Append,
Delete,
}
pub struct Input {
pub text: String,
pub prompt: String,
idx: usize,
offset: usize,
cursor_position: u16,
pub cursor_offset: u16,
available_width: usize,
change: Option<InputChange>,
}
impl Default for Input {
fn default() -> Self {
Self {
text: String::new(),
prompt: String::new(),
idx: 0,
offset: 0,
cursor_position: 0,
cursor_offset: 0,
available_width: usize::MAX,
change: None,
}
}
}
impl Input {
pub fn new(prompt: &str) -> Self {
Self {
prompt: prompt.to_owned(),
cursor_offset: prompt.len() as u16,
..Self::default()
}
}
pub fn set_text(&mut self, text: &str) {
text.clone_into(&mut self.text);
self.set_idx(text.len());
}
pub fn set_idx(&mut self, idx: usize) {
self.idx = if idx > self.text.len() {
self.text.len()
} else {
idx
};
self.cursor_position = self.text[..self.idx].width() as u16;
self.check_lower_bound();
self.check_higher_bound();
}
fn clear_range<R: RangeBounds<usize>>(&mut self, range: R) {
if self.text.drain(range).next().is_some() {
self.change = Some(InputChange::Delete);
}
}
fn insert_key(&mut self, ch: char) {
if self.idx == self.text.len() {
self.text.push(ch);
self.change = Some(InputChange::Append);
} else {
self.text.insert(self.idx, ch);
self.change = Some(InputChange::Insert);
}
self.idx += ch.len_utf8();
self.cursor_position += ch.width().unwrap() as u16;
self.check_higher_bound();
}
fn pop_key(&mut self) {
if self.idx == 0 {
return;
}
let (offset, ch) = self.text[..self.idx].grapheme_indices(true).next_back().unwrap();
self.cursor_position -= ch.width() as u16;
self.clear_range(offset..self.idx);
self.idx = offset;
self.check_lower_bound();
}
fn move_cursor_left(&mut self) {
if self.idx == 0 {
return;
}
let (offset, ch) = self.text[..self.idx].grapheme_indices(true).next_back().unwrap();
self.cursor_position -= ch.width() as u16;
self.idx = offset;
self.check_lower_bound();
}
fn move_cursor_right(&mut self) {
if self.idx == self.text.len() {
return;
}
let (offset, ch) = self.text[self.idx..]
.grapheme_indices(true)
.next()
.map(|(offset, ch)| (self.idx + offset + ch.len(), ch))
.unwrap();
self.cursor_position += ch.width() as u16;
self.idx = offset;
self.check_higher_bound();
}
fn move_cursor_one_word_left(&mut self) {
let idx = self.text[..self.idx]
.unicode_word_indices()
.next_back()
.map_or(0, |(offset, _)| offset);
self.cursor_position -= self.text[idx..self.idx].width() as u16;
self.idx = idx;
self.check_lower_bound();
}
fn move_cursor_one_word_right(&mut self) {
let old_idx = self.idx;
self.idx = self.text[self.idx..]
.unicode_word_indices()
.nth(1)
.map_or(self.text.len(), |(offset, _)| self.idx + offset);
self.cursor_position += self.text[old_idx..self.idx].width() as u16;
self.check_higher_bound();
}
fn move_cursor_to_beginning_of_line(&mut self) {
self.idx = 0;
self.offset = 0;
self.cursor_position = 0;
}
fn move_cursor_to_end_of_line(&mut self) {
self.idx = self.text.len();
self.cursor_position = self.text.width() as u16;
self.check_higher_bound();
}
fn delete_word_before_cursor(&mut self) {
let old_idx = self.idx;
self.move_cursor_one_word_left();
self.clear_range(self.idx..old_idx);
self.check_lower_bound();
}
fn clear_line(&mut self) {
if !self.text.is_empty() {
self.text.clear();
self.idx = 0;
self.offset = 0;
self.cursor_position = 0;
self.change = Some(InputChange::Delete);
}
}
fn clear_to_right(&mut self) {
self.clear_range(self.idx..);
}
fn update(&mut self, key: KeyEvent) -> bool {
self.change = None;
match (key.code, key.modifiers) {
(KeyCode::Left, KeyModifiers::CONTROL) => self.move_cursor_one_word_left(),
(KeyCode::Right, KeyModifiers::CONTROL) => self.move_cursor_one_word_right(),
(KeyCode::Left, _) | (KeyCode::Char('b'), KeyModifiers::CONTROL) => {
self.move_cursor_left();
}
(KeyCode::Right, _) | (KeyCode::Char('f'), KeyModifiers::CONTROL) => {
self.move_cursor_right();
}
(KeyCode::Char('a'), KeyModifiers::CONTROL) => {
self.move_cursor_to_beginning_of_line();
}
(KeyCode::Char('e'), KeyModifiers::CONTROL) => self.move_cursor_to_end_of_line(),
(KeyCode::Char('w'), KeyModifiers::CONTROL) => self.delete_word_before_cursor(),
(KeyCode::Char('u'), KeyModifiers::CONTROL) => self.clear_line(),
(KeyCode::Char('k'), KeyModifiers::CONTROL) => self.clear_to_right(),
(KeyCode::Backspace, _) | (KeyCode::Char('h'), KeyModifiers::CONTROL) => {
self.pop_key();
}
(KeyCode::Char(c), _) => self.insert_key(c),
_ => {}
}
self.change.is_some()
}
pub fn update_width(&mut self, width: u16) {
self.available_width = usize::from(width.saturating_sub(self.cursor_offset));
self.check_higher_bound();
}
fn check_lower_bound(&mut self) {
self.offset = self.offset.min(self.idx);
}
fn check_higher_bound(&mut self) {
let substring = &self.text[self.offset..self.idx];
let mut width = substring.width();
for (offset, ch) in substring.grapheme_indices(true) {
if width < self.available_width {
self.offset += offset;
return;
}
width -= ch.width();
}
}
pub fn visible_width(&self) -> usize {
self.text[self.offset..].width()
}
pub fn cursor_position(&self) -> u16 {
self.cursor_position + self.cursor_offset - self.text[..self.offset].width() as u16
}
}
impl From<&Mode> for Input {
fn from(value: &Mode) -> Self {
let prompt = match value {
Mode::Search(_) => "/",
Mode::GameCreation => "Game Name: ",
Mode::ProfileCreation => "Profile Name: ",
Mode::EntryRenaming | Mode::ProfileRenaming | Mode::GameRenaming => "Rename: ",
Mode::FolderCreation(_) => "Folder Name: ",
Mode::Normal => "",
_ => panic!(),
};
Self::new(prompt)
}
}
impl Display for Input {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.text[self.offset..])
}
}