feat: add word implemented + refactor of handlers as an organization test

This commit is contained in:
2026-08-03 22:37:51 +02:00
parent 8231ae3ad1
commit c0b6ec1f97
3 changed files with 317 additions and 140 deletions

View File

@@ -1,18 +1,15 @@
use crate::modules::source::{FileDatabase, Word, WordSource};
use crossterm::event::{
self,
Event::{self},
};
use crate::modules::ui::handler::handle_user_input;
use ratatui::{
DefaultTerminal, Frame,
layout::{Constraint, Direction, Flex, Layout},
style::{Color, Style},
text::Text,
widgets::{Block, Borders, List, ListDirection, ListItem, ListState, Padding, Paragraph},
widgets::{Block, Borders, List, ListDirection, ListItem, ListState, Padding, Paragraph, Wrap},
};
use std::io;
#[derive(PartialEq)]
#[derive(Copy, Clone, PartialEq)]
pub enum UiState {
LIST,
FILTERWORD,
@@ -22,18 +19,29 @@ pub enum UiState {
DELETE,
}
#[derive(Copy, Clone, PartialEq)]
pub enum AddState {
KEY,
DESCRIPTION,
LANG,
}
pub struct Preferences {
help: bool,
}
pub struct App {
state: UiState,
file_database: FileDatabase,
list_state: ListState,
word_list: Vec<Word>,
word_filter: Option<String>,
lang_filter: Option<String>,
exit: bool,
pub state: UiState,
pub file_database: FileDatabase,
pub list_state: ListState,
pub add_state: AddState,
pub description_scroll: u16,
pub word_list: Vec<Word>,
pub word_to_add: Option<Word>,
pub word_to_edit: Option<Word>,
pub word_filter: Option<String>,
pub lang_filter: Option<String>,
pub exit: bool,
}
impl App {
@@ -44,7 +52,11 @@ impl App {
state: UiState::LIST,
file_database: db,
list_state: ListState::default().with_selected(Some(0)),
add_state: AddState::KEY,
description_scroll: 0,
word_list: wl,
word_to_add: None,
word_to_edit: None,
word_filter: None,
lang_filter: None,
exit: false,
@@ -74,20 +86,15 @@ impl App {
])
.split(frame.area());
/*
* Filters block
* */
let filters_layout = Layout::default()
.direction(Direction::Horizontal)
.spacing(1)
.constraints(vec![Constraint::Fill(1), Constraint::Length(8)])
.split(main_layout[0]);
let words_layout =
Layout::horizontal(vec![Constraint::Percentage(30), Constraint::Percentage(70)])
.split(main_layout[1]);
let help_layout = Layout::horizontal(vec![Constraint::Fill(1)])
.flex(Flex::Center)
.split(main_layout[2]);
let word_filter: String = {
if let Some(l) = &self.word_filter {
l.to_string()
@@ -113,6 +120,40 @@ impl App {
filters_layout[1],
);
/*
* Word list / add / edit block
* */
if self.state == UiState::ADD {
let add_words_heading_layout =
Layout::horizontal(vec![Constraint::Fill(1), Constraint::Length(8)])
.spacing(1)
.split(main_layout[0]);
// PERF: maybe not clone
let word_to_add = self.word_to_add.clone().unwrap_or(Word {
key: "".to_string(),
description: "".to_string(),
lang: "".to_string(),
});
frame.render_widget(
Paragraph::new(word_to_add.key)
.block(Block::new().title("Word").borders(Borders::ALL)),
add_words_heading_layout[0],
);
frame.render_widget(
Paragraph::new(word_to_add.lang)
.block(Block::new().title("Lang").borders(Borders::ALL)),
add_words_heading_layout[1],
);
frame.render_widget(
Paragraph::new(word_to_add.description)
.block(Block::new().title("Description").borders(Borders::ALL)),
main_layout[1],
);
} else {
let words_layout =
Layout::horizontal(vec![Constraint::Percentage(30), Constraint::Percentage(70)])
.split(main_layout[1]);
let word_key_list_items: Vec<ListItem> = self
.word_list
.clone()
@@ -144,10 +185,21 @@ impl App {
let selected_word = self.word_list.clone().into_iter().nth(scroll_position);
if let Some(w) = selected_word {
frame.render_widget(
Paragraph::new(w.description).block(Block::new().borders(Borders::ALL)),
Paragraph::new(w.description)
.wrap(Wrap { trim: true })
.scroll((self.description_scroll, 0))
.block(Block::new().borders(Borders::ALL)),
words_layout[1],
);
}
}
/*
* Help block
* */
let help_layout = Layout::horizontal(vec![Constraint::Fill(1)])
.flex(Flex::Center)
.split(main_layout[2]);
let help_text = Text::raw("(f)ilter | (l)ang | (d)elete | (e)dit | (a)dd | (q)uit ")
.style(Style::new().fg(Color::DarkGray));
@@ -159,7 +211,7 @@ impl App {
frame.render_widget(help_text, help_area);
}
fn update_word_list(&mut self) {
pub fn update_word_list(&mut self) {
let word_list = {
if let Some(w_filter) = &self.word_filter
&& let Some(l_filter) = &self.lang_filter
@@ -175,96 +227,11 @@ impl App {
}
};
self.list_state.select(Some(0));
self.description_scroll = 0;
self.word_list = word_list;
}
fn handle_events(&mut self) -> io::Result<()> {
if let Event::Key(key) = event::read()? {
match key.code {
event::KeyCode::Char(c) => {
match self.state {
UiState::FILTERWORD => {
if let Some(mut prev_word) = self.word_filter.clone() {
prev_word.push(c);
self.word_filter = Some(prev_word);
} else {
self.word_filter = Some(c.to_string());
}
self.update_word_list();
}
UiState::FILTERLANG => {
if let Some(mut prev_lang) = self.lang_filter.clone() {
prev_lang.push(c);
self.lang_filter = Some(prev_lang);
} else {
self.lang_filter = Some(c.to_string());
}
self.update_word_list();
}
_ => {
// Quit
if c == 'q' || c == 'x' {
return Err(io::Error::last_os_error()); // FIXME: this is not right
}
// State change
if c == 'f' || c == '/' {
self.state = UiState::FILTERWORD;
}
if c == 'l' {
self.state = UiState::FILTERLANG;
}
if c == 'd' {
self.state = UiState::DELETE;
}
if c == 'e' {
self.state = UiState::EDIT;
}
if c == 'a' {
self.state = UiState::ADD;
}
// Movement
if c == 'j' {
self.list_state.select_next();
}
if c == 'k' {
self.list_state.select_previous();
}
}
}
}
event::KeyCode::Backspace => match self.state {
UiState::FILTERWORD => {
if let Some(w) = &self.word_filter {
if w.len() <= 1 {
self.word_filter = None;
} else {
let mut new_w = w.clone();
new_w.pop();
self.word_filter = Some(new_w);
}
self.update_word_list();
}
}
UiState::FILTERLANG => {
if let Some(l) = &self.lang_filter {
if l.len() <= 1 {
self.lang_filter = None;
} else {
let mut new_l = l.clone();
new_l.pop();
self.lang_filter = Some(new_l);
}
self.update_word_list();
}
}
_ => {}
},
event::KeyCode::Esc => {
self.state = UiState::LIST;
}
_ => return Ok(()),
}
}
Ok(())
return handle_user_input(self);
}
}

209
src/modules/ui/handler.rs Normal file
View File

@@ -0,0 +1,209 @@
use std::io;
use crossterm::event::{self, Event};
use crate::modules::{
source::{Word, WordSource},
ui::app::{AddState, App, UiState},
};
pub fn handle_user_input(app: &mut App) -> io::Result<()> {
if let Event::Key(key) = event::read()? {
match key.code {
event::KeyCode::Char(c) => {
match app.state {
UiState::FILTERWORD => {
if let Some(mut prev_word) = app.word_filter.clone() {
prev_word.push(c);
app.word_filter = Some(prev_word);
} else {
app.word_filter = Some(c.to_string());
}
app.update_word_list();
}
UiState::FILTERLANG => {
if let Some(mut prev_lang) = app.lang_filter.clone() {
prev_lang.push(c);
app.lang_filter = Some(prev_lang);
} else {
app.lang_filter = Some(c.to_string());
}
app.update_word_list();
}
UiState::ADD => match app.add_state {
AddState::KEY => {
if let Some(mut prev_word) = app.word_to_add.clone() {
prev_word.key.push(c);
app.word_to_add = Some(prev_word);
} else {
app.word_to_add = Some(Word {
key: c.to_string(),
description: "".to_string(),
lang: "".to_string(),
});
}
}
AddState::DESCRIPTION => {
if let Some(mut prev_word) = app.word_to_add.clone() {
prev_word.description.push(c);
app.word_to_add = Some(prev_word);
} else {
app.word_to_add = Some(Word {
key: c.to_string(),
description: "".to_string(),
lang: "".to_string(),
});
}
}
AddState::LANG => {
if let Some(mut prev_word) = app.word_to_add.clone() {
prev_word.lang.push(c);
app.word_to_add = Some(prev_word);
} else {
app.word_to_add = Some(Word {
key: c.to_string(),
description: "".to_string(),
lang: "".to_string(),
});
}
}
},
_ => {
// Quit
if c == 'q' || c == 'x' {
return Err(io::Error::last_os_error()); // FIXME: this is not right
}
// State change
if c == 'f' || c == '/' {
app.state = UiState::FILTERWORD;
}
if c == 'l' {
app.state = UiState::FILTERLANG;
}
if c == 'd' {
app.state = UiState::DELETE;
}
if c == 'e' {
app.state = UiState::EDIT;
}
if c == 'a' {
app.state = UiState::ADD;
}
// Movement
if c == 'j' {
app.list_state.select_next();
app.description_scroll = 0;
}
if c == 'k' {
app.list_state.select_previous();
app.description_scroll = 0;
}
if c == 'J' {
app.description_scroll += 1;
}
if c == 'K' {
if app.description_scroll > 0 {
app.description_scroll -= 1;
}
}
}
}
}
event::KeyCode::Enter => match app.state {
UiState::ADD => {
if let Some(w) = app.word_to_add.clone()
&& w.key.len() > 0
{
let res = app.file_database.insert_word(w);
match res {
Ok(_) => {
app.update_word_list();
app.word_to_add = None;
app.state = get_next_ui_state(app.state);
}
Err(_) => app.state = get_next_ui_state(app.state),
}
}
}
_ => {}
},
event::KeyCode::Tab => match app.state {
UiState::ADD => {
app.add_state = get_next_add_state(app.add_state);
}
_ => {
app.state = get_next_ui_state(app.state);
}
},
event::KeyCode::Backspace => match app.state {
UiState::FILTERWORD => {
app.word_filter = remove_last_char(&app.word_filter);
app.update_word_list();
}
UiState::FILTERLANG => {
app.lang_filter = remove_last_char(&app.lang_filter);
app.update_word_list();
}
UiState::ADD => match app.add_state {
AddState::KEY => {
if let Some(mut prev_word) = app.word_to_add.clone() {
prev_word.key.pop();
app.word_to_add = Some(prev_word);
}
}
AddState::DESCRIPTION => {
if let Some(mut prev_word) = app.word_to_add.clone() {
prev_word.description.pop();
app.word_to_add = Some(prev_word);
}
}
AddState::LANG => {
if let Some(mut prev_word) = app.word_to_add.clone() {
prev_word.lang.pop();
app.word_to_add = Some(prev_word);
}
}
},
_ => {}
},
event::KeyCode::Esc => {
app.state = UiState::LIST;
}
_ => return Ok(()),
}
}
Ok(())
}
fn remove_last_char(input: &Option<String>) -> Option<String> {
if let Some(l) = input {
if l.len() <= 1 {
None
} else {
let mut new_l = l.clone();
new_l.pop();
Some(new_l)
}
} else {
None
}
}
fn get_next_add_state(current: AddState) -> AddState {
if current == AddState::KEY {
AddState::LANG
} else if current == AddState::LANG {
AddState::DESCRIPTION
} else {
AddState::KEY
}
}
fn get_next_ui_state(current: UiState) -> UiState {
match current {
UiState::LIST => UiState::FILTERWORD,
UiState::FILTERWORD => UiState::FILTERLANG,
UiState::FILTERLANG => UiState::LIST,
_ => UiState::LIST,
}
}

View File

@@ -1 +1,2 @@
pub mod app;
pub mod handler;