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]]
|
[[package]]
|
||||||
name = "lemors"
|
name = "lemurs"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argh",
|
"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::{
|
use crossterm::{
|
||||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers},
|
||||||
execute,
|
execute,
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
};
|
};
|
||||||
|
@ -13,7 +13,9 @@ use tui::{
|
||||||
};
|
};
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
mod input_field;
|
||||||
mod window_manager_selector;
|
mod window_manager_selector;
|
||||||
|
use input_field::{InputFieldDisplayType, InputFieldWidget};
|
||||||
use window_manager_selector::{WindowManager, WindowManagerSelectorWidget};
|
use window_manager_selector::{WindowManager, WindowManagerSelectorWidget};
|
||||||
|
|
||||||
enum InputMode {
|
enum InputMode {
|
||||||
|
@ -29,10 +31,10 @@ struct App {
|
||||||
window_manager_widget: WindowManagerSelectorWidget,
|
window_manager_widget: WindowManagerSelectorWidget,
|
||||||
|
|
||||||
/// Current value of the Username
|
/// Current value of the Username
|
||||||
username: String,
|
username_widget: InputFieldWidget,
|
||||||
|
|
||||||
/// Current value of the Password
|
/// Current value of the Password
|
||||||
password: String,
|
password_widget: InputFieldWidget,
|
||||||
|
|
||||||
/// Current input mode
|
/// Current input mode
|
||||||
input_mode: InputMode,
|
input_mode: InputMode,
|
||||||
|
@ -46,8 +48,8 @@ impl Default for App {
|
||||||
WindowManager::new("i3", "/usr/bin/i3"),
|
WindowManager::new("i3", "/usr/bin/i3"),
|
||||||
WindowManager::new("awesome", "/usr/bin/awesome"),
|
WindowManager::new("awesome", "/usr/bin/awesome"),
|
||||||
]),
|
]),
|
||||||
username: String::new(),
|
username_widget: InputFieldWidget::new("Username", InputFieldDisplayType::Echo),
|
||||||
password: String::new(),
|
password_widget: InputFieldWidget::new("Password", InputFieldDisplayType::Replace('*')),
|
||||||
input_mode: InputMode::Normal,
|
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 {
|
InputMode::WindowManager => match key.code {
|
||||||
KeyCode::Enter | KeyCode::Tab => {
|
KeyCode::Enter | KeyCode::Tab | KeyCode::Down => {
|
||||||
app.input_mode = InputMode::Username;
|
app.input_mode = InputMode::Username;
|
||||||
}
|
}
|
||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
app.input_mode = InputMode::Normal;
|
app.input_mode = InputMode::Normal;
|
||||||
}
|
}
|
||||||
KeyCode::Left => {
|
KeyCode::Left | KeyCode::Char('h') => {
|
||||||
app.window_manager_widget.left();
|
app.window_manager_widget.left();
|
||||||
}
|
}
|
||||||
KeyCode::Right => {
|
KeyCode::Right | KeyCode::Char('l') => {
|
||||||
app.window_manager_widget.right();
|
app.window_manager_widget.right();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
InputMode::Username => match key.code {
|
InputMode::Username => match key.code {
|
||||||
KeyCode::Enter | KeyCode::Tab => {
|
KeyCode::Enter | KeyCode::Down => {
|
||||||
app.input_mode = InputMode::Password;
|
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 => {
|
KeyCode::Esc => {
|
||||||
app.input_mode = InputMode::Normal;
|
app.input_mode = InputMode::Normal;
|
||||||
}
|
}
|
||||||
KeyCode::Char(c) => {
|
key_code => app.username_widget.key_press(key_code),
|
||||||
app.username.push(c);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
},
|
},
|
||||||
InputMode::Password => match key.code {
|
InputMode::Password => match key.code {
|
||||||
KeyCode::Enter | KeyCode::Tab => {
|
KeyCode::Enter => {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
KeyCode::Tab => {
|
||||||
|
if key.modifiers == KeyModifiers::SHIFT {
|
||||||
|
app.input_mode = InputMode::Username;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Up => {
|
||||||
|
app.input_mode = InputMode::Username;
|
||||||
|
}
|
||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
app.input_mode = InputMode::Normal;
|
app.input_mode = InputMode::Normal;
|
||||||
}
|
}
|
||||||
KeyCode::Char(c) => {
|
key_code => app.password_widget.key_press(key_code),
|
||||||
app.password.push(c);
|
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -166,35 +181,9 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||||
matches!(app.input_mode, InputMode::WindowManager),
|
matches!(app.input_mode, InputMode::WindowManager),
|
||||||
);
|
);
|
||||||
|
|
||||||
let username = Paragraph::new(app.username.as_ref())
|
app.username_widget
|
||||||
.style(match app.input_mode {
|
.render(f, chunks[4], matches!(app.input_mode, InputMode::Username));
|
||||||
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]);
|
|
||||||
|
|
||||||
let password = Paragraph::new(app.password.as_ref())
|
app.password_widget
|
||||||
.style(match app.input_mode {
|
.render(f, chunks[6], matches!(app.input_mode, InputMode::Password));
|
||||||
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,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue