mirror of
https://github.com/coastalwhite/lemurs
synced 2024-11-10 13:14:27 +00:00
Input fields and KeyCodes
This commit is contained in:
parent
9f58364bb2
commit
3ba23fff7f
3 changed files with 233 additions and 48 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -129,7 +129,7 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "lemors"
|
||||
name = "lemurs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"argh",
|
||||
|
|
196
src/input_field.rs
Normal file
196
src/input_field.rs
Normal file
|
@ -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<impl tui::backend::Backend>,
|
||||
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(), "");
|
||||
}
|
||||
}
|
83
src/main.rs
83
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<B: Backend>(terminal: &mut Terminal<B>, 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<B: Backend>(f: &mut Frame<B>, 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));
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue