diff --git a/src/modules/ui/app.rs b/src/modules/ui/app.rs index 6ec341b..92afb2b 100644 --- a/src/modules/ui/app.rs +++ b/src/modules/ui/app.rs @@ -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_filter: Option, - lang_filter: Option, - 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, + pub word_to_add: Option, + pub word_to_edit: Option, + pub word_filter: Option, + pub lang_filter: Option, + 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,42 +120,87 @@ impl App { filters_layout[1], ); - let word_key_list_items: Vec = self - .word_list - .clone() - .iter() - .map(|w| ListItem::new(w.key.clone())) - .collect(); + /* + * 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]); - let word_key_list = List::new(word_key_list_items) - .direction(ListDirection::TopToBottom) - .block( - Block::default() - .title("Entries") - .borders(Borders::ALL) - .padding(Padding::horizontal(1)), - ) - .highlight_style(Style::default().fg(Color::default())) - .highlight_symbol("> ") - .style(Style::default().fg(Color::DarkGray)); - frame.render_stateful_widget(word_key_list, words_layout[0], &mut self.list_state); - - let scroll_position: usize = { - if let Some(i) = self.list_state.selected() { - i - } else { - 0 - } - }; - // PERF: do not clone - let selected_word = self.word_list.clone().into_iter().nth(scroll_position); - if let Some(w) = selected_word { + // 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(w.description).block(Block::new().borders(Borders::ALL)), - words_layout[1], + 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 = self + .word_list + .clone() + .iter() + .map(|w| ListItem::new(w.key.clone())) + .collect(); + + let word_key_list = List::new(word_key_list_items) + .direction(ListDirection::TopToBottom) + .block( + Block::default() + .title("Entries") + .borders(Borders::ALL) + .padding(Padding::horizontal(1)), + ) + .highlight_style(Style::default().fg(Color::default())) + .highlight_symbol("> ") + .style(Style::default().fg(Color::DarkGray)); + frame.render_stateful_widget(word_key_list, words_layout[0], &mut self.list_state); + + let scroll_position: usize = { + if let Some(i) = self.list_state.selected() { + i + } else { + 0 + } + }; + // PERF: do not clone + 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) + .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)); let help_area = help_layout[0].centered( @@ -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); } } diff --git a/src/modules/ui/handler.rs b/src/modules/ui/handler.rs new file mode 100644 index 0000000..fa710d9 --- /dev/null +++ b/src/modules/ui/handler.rs @@ -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) -> Option { + 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, + } +} diff --git a/src/modules/ui/mod.rs b/src/modules/ui/mod.rs index 309be62..55f41a0 100644 --- a/src/modules/ui/mod.rs +++ b/src/modules/ui/mod.rs @@ -1 +1,2 @@ pub mod app; +pub mod handler;