diff --git a/Cargo.lock b/Cargo.lock index 2ef34e3..e719ec1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -129,7 +129,7 @@ dependencies = [ ] [[package]] -name = "lemors" +name = "lemurs" version = "0.1.0" dependencies = [ "argh", diff --git a/src/input_field.rs b/src/input_field.rs new file mode 100644 index 0000000..a192734 --- /dev/null +++ b/src/input_field.rs @@ -0,0 +1,196 @@ +use crossterm::event::KeyCode; +use tui::{ + layout::{Alignment, Rect}, + style::{Color, Style}, + terminal::Frame, + text::{Span, Spans, Text}, + widgets::{Block, Borders, Paragraph}, +}; +use unicode_width::UnicodeWidthStr; + +pub enum InputFieldDisplayType { + Echo, + Replace(char), +} + +pub struct InputFieldWidget { + title: String, + content: String, + cursor: u16, + display_type: InputFieldDisplayType, +} + +impl InputFieldWidget { + pub fn new(title: impl ToString, display_type: InputFieldDisplayType) -> Self { + let title = title.to_string(); + + Self { + title, + content: String::new(), + cursor: 0, + display_type, + } + } + + #[inline] + fn len(&self) -> usize { + self.content.len() + } + + fn show_string(&self) -> String { + use InputFieldDisplayType::{Echo, Replace}; + + match self.display_type { + Echo => self.content.clone(), + Replace(character) => character.to_string().repeat(self.len()), + } + } + + fn backspace(&mut self) { + if self.cursor == 0 { + return; + } + + debug_assert!(usize::from(self.cursor) <= self.len()); + self.cursor -= 1; + self.content.remove(self.cursor.into()); + } + + fn delete(&mut self) { + if usize::from(self.cursor) == self.len() { + return; + } + + debug_assert!(usize::from(self.cursor) <= self.len()); + self.content.remove(self.cursor.into()); + } + + fn insert(&mut self, character: char) { + // Make sure the cursor doesn't overflow + if self.len() == usize::from(u16::MAX) { + return; + } + + debug_assert!(usize::from(self.cursor) <= self.len()); + self.content.insert(self.cursor.into(), character); + self.cursor += 1; + } + + fn right(&mut self) { + if usize::from(self.cursor) == self.len() { + return; + } + + self.cursor += 1; + } + + fn left(&mut self) { + if self.cursor == 0 { + return; + } + + self.cursor -= 1; + } + + pub fn render( + &self, + frame: &mut Frame, + area: Rect, + is_focused: bool, + ) { + let show_string = self.show_string(); + let widget = Paragraph::new(show_string.as_ref()) + .style(if is_focused { + Style::default().fg(Color::Yellow) + } else { + Style::default() + }) + .block( + Block::default() + .borders(Borders::ALL) + .title(self.title.clone()), + ); + + frame.render_widget(widget, area); + + if is_focused { + frame.set_cursor(area.x + self.content[..usize::from(self.cursor)].width() as u16 + 1, area.y + 1); + } + } + + pub fn key_press(&mut self, key_code: KeyCode) { + match key_code { + KeyCode::Backspace => self.backspace(), + KeyCode::Delete => self.delete(), + + KeyCode::Left => self.left(), + KeyCode::Right => self.right(), + + KeyCode::Char(c) => self.insert(c), + _ => {} + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use InputFieldDisplayType::*; + + #[test] + fn cursor_movement() { + // TODO: Verify Unicode behaviour + let mut input_field = InputFieldWidget::new("", Echo); + assert_eq!(input_field.cursor, 0); + input_field.insert('x'); + assert_eq!(input_field.cursor, 1); + input_field.insert('x'); + assert_eq!(input_field.cursor, 2); + input_field.insert('x'); + assert_eq!(input_field.cursor, 3); + input_field.insert('x'); + assert_eq!(input_field.cursor, 4); + input_field.right(); + assert_eq!(input_field.cursor, 4); + input_field.left(); + assert_eq!(input_field.cursor, 3); + input_field.left(); + assert_eq!(input_field.cursor, 2); + input_field.left(); + assert_eq!(input_field.cursor, 1); + input_field.left(); + assert_eq!(input_field.cursor, 0); + input_field.left(); + assert_eq!(input_field.cursor, 0); + input_field.right(); + assert_eq!(input_field.cursor, 1); + input_field.backspace(); + assert_eq!(input_field.cursor, 0); + } + + #[test] + fn integration() { + let mut input_field = InputFieldWidget::new("", Echo); + assert_eq!(&input_field.show_string(), ""); + input_field.backspace(); + assert_eq!(&input_field.show_string(), ""); + input_field.insert('x'); + assert_eq!(&input_field.show_string(), "x"); + input_field.insert('y'); + assert_eq!(&input_field.show_string(), "xy"); + input_field.backspace(); + assert_eq!(&input_field.show_string(), "x"); + input_field.insert('y'); + assert_eq!(&input_field.show_string(), "xy"); + input_field.left(); + assert_eq!(&input_field.show_string(), "xy"); + input_field.backspace(); + assert_eq!(&input_field.show_string(), "y"); + input_field.backspace(); + assert_eq!(&input_field.show_string(), "y"); + input_field.right(); + assert_eq!(&input_field.show_string(), "y"); + input_field.backspace(); + assert_eq!(&input_field.show_string(), ""); + } +} diff --git a/src/main.rs b/src/main.rs index 6d1b436..2319893 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; @@ -13,7 +13,9 @@ use tui::{ }; use unicode_width::UnicodeWidthStr; +mod input_field; mod window_manager_selector; +use input_field::{InputFieldDisplayType, InputFieldWidget}; use window_manager_selector::{WindowManager, WindowManagerSelectorWidget}; enum InputMode { @@ -29,10 +31,10 @@ struct App { window_manager_widget: WindowManagerSelectorWidget, /// Current value of the Username - username: String, + username_widget: InputFieldWidget, /// Current value of the Password - password: String, + password_widget: InputFieldWidget, /// Current input mode input_mode: InputMode, @@ -46,8 +48,8 @@ impl Default for App { WindowManager::new("i3", "/usr/bin/i3"), WindowManager::new("awesome", "/usr/bin/awesome"), ]), - username: String::new(), - password: String::new(), + username_widget: InputFieldWidget::new("Username", InputFieldDisplayType::Echo), + password_widget: InputFieldWidget::new("Password", InputFieldDisplayType::Replace('*')), input_mode: InputMode::Normal, } } @@ -97,42 +99,55 @@ fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<( _ => {} }, InputMode::WindowManager => match key.code { - KeyCode::Enter | KeyCode::Tab => { + KeyCode::Enter | KeyCode::Tab | KeyCode::Down => { app.input_mode = InputMode::Username; } KeyCode::Esc => { app.input_mode = InputMode::Normal; } - KeyCode::Left => { + KeyCode::Left | KeyCode::Char('h') => { app.window_manager_widget.left(); } - KeyCode::Right => { + KeyCode::Right | KeyCode::Char('l') => { app.window_manager_widget.right(); } _ => {} }, InputMode::Username => match key.code { - KeyCode::Enter | KeyCode::Tab => { + KeyCode::Enter | KeyCode::Down => { app.input_mode = InputMode::Password; } + KeyCode::Tab => { + if key.modifiers == KeyModifiers::SHIFT { + app.input_mode = InputMode::WindowManager; + } else { + app.input_mode = InputMode::Password; + } + } + KeyCode::Up => { + app.input_mode = InputMode::WindowManager; + } KeyCode::Esc => { app.input_mode = InputMode::Normal; } - KeyCode::Char(c) => { - app.username.push(c); - } - _ => {} + key_code => app.username_widget.key_press(key_code), }, InputMode::Password => match key.code { - KeyCode::Enter | KeyCode::Tab => { + KeyCode::Enter => { todo!() } + KeyCode::Tab => { + if key.modifiers == KeyModifiers::SHIFT { + app.input_mode = InputMode::Username; + } + } + KeyCode::Up => { + app.input_mode = InputMode::Username; + } KeyCode::Esc => { app.input_mode = InputMode::Normal; } - KeyCode::Char(c) => { - app.password.push(c); - } + key_code => app.password_widget.key_press(key_code), _ => {} }, } @@ -166,35 +181,9 @@ fn ui(f: &mut Frame, app: &App) { matches!(app.input_mode, InputMode::WindowManager), ); - let username = Paragraph::new(app.username.as_ref()) - .style(match app.input_mode { - InputMode::Normal => Style::default(), - InputMode::WindowManager => Style::default(), - InputMode::Username => Style::default().fg(Color::Yellow), - InputMode::Password => Style::default(), - }) - .block(Block::default().borders(Borders::ALL).title("Input")); - f.render_widget(username, chunks[4]); + app.username_widget + .render(f, chunks[4], matches!(app.input_mode, InputMode::Username)); - let password = Paragraph::new(app.password.as_ref()) - .style(match app.input_mode { - InputMode::Normal => Style::default(), - InputMode::WindowManager => Style::default(), - InputMode::Username => Style::default(), - InputMode::Password => Style::default().fg(Color::Yellow), - }) - .block(Block::default().borders(Borders::ALL).title("Password")); - f.render_widget(password, chunks[6]); - - match app.input_mode { - InputMode::Normal | InputMode::WindowManager => {} - InputMode::Username => f.set_cursor( - chunks[4].x + app.username.width() as u16 + 1, - chunks[4].y + 1, - ), - InputMode::Password => f.set_cursor( - chunks[6].x + app.password.width() as u16 + 1, - chunks[6].y + 1, - ), - } + app.password_widget + .render(f, chunks[6], matches!(app.input_mode, InputMode::Password)); }