use crate::modules::source::{FileDatabase, Word, WordSource}; use crate::modules::ui::app::UiState::FILTERLANG; use crate::modules::ui::handler::handle_user_input; use crossterm::event::KeyEvent; use ratatui::{ DefaultTerminal, Frame, layout::{Constraint, Direction, Flex, Layout}, style::{Color, Style}, text::Text, widgets::{Block, Borders, List, ListDirection, ListItem, ListState, Padding, Paragraph, Wrap}, }; use std::io; #[derive(Copy, Clone, PartialEq)] pub enum UiState { LIST, FILTERWORD, FILTERLANG, EDIT, ADD, DELETE, } #[derive(Copy, Clone, PartialEq)] pub enum AddState { KEY, DESCRIPTION, LANG, } pub struct Preferences { help: bool, } pub struct App { 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 last_key: Option, pub exit: bool, } impl App { pub fn new(db: FileDatabase) -> Self { let wl = db.get_all_words(); Self { 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, last_key: None, exit: false, } } pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> { while !self.exit { terminal.draw(|frame| self.draw(frame))?; let event_result = self.handle_events(); if let Err(_) = event_result { break; } } Ok(()) } fn draw(&mut self, frame: &mut Frame) { let main_layout = Layout::default() .direction(Direction::Vertical) .spacing(0) .margin(1) .constraints(vec![ Constraint::Length(3), Constraint::Fill(1), Constraint::Length(1), ]) .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 word_filter: String = { if let Some(l) = &self.word_filter { l.to_string() } else { "".to_string() } }; frame.render_widget( Paragraph::new(word_filter.clone()).block( Block::new() .title(self.format_ui_block_title("Word", UiState::FILTERWORD)) .borders(Borders::ALL), ), filters_layout[0], ); let lang_filter: String = { if let Some(l) = &self.lang_filter { l.to_string() } else { "".to_string() } }; frame.render_widget( Paragraph::new(lang_filter).block( Block::new() .title(self.format_ui_block_title("Lang", UiState::FILTERLANG)) .borders(Borders::ALL), ), filters_layout[1], ); /* * Word list / add / edit block * */ 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 self.state == UiState::ADD || self.state == UiState::EDIT { 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 = { if self.state == UiState::EDIT && let Some(w) = self.word_to_edit.clone() { w } else { 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(self.format_add_block_title("Word", AddState::KEY)) .borders(Borders::ALL), ), add_words_heading_layout[0], ); frame.render_widget( Paragraph::new(word_to_add.lang).block( Block::new() .title(self.format_add_block_title("Lang", AddState::LANG)) .borders(Borders::ALL), ), add_words_heading_layout[1], ); frame.render_widget( Paragraph::new(word_to_add.description).block( Block::new() .title(self.format_add_block_title("Description", AddState::DESCRIPTION)) .borders(Borders::ALL), ), main_layout[1], ); } else { let highligh_symbol: &str = { if self.state == UiState::LIST { "> " } else if self.state == UiState::DELETE { "x " } else { " " } }; let highligh_color: Color = { if self.state == UiState::LIST { Color::Yellow } else if self.state == UiState::DELETE { Color::LightRed } else { Color::default() } }; 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(self.format_ui_block_title("Entries", UiState::LIST)) .borders(Borders::ALL) .padding(Padding::horizontal(1)), ) .highlight_style(Style::default().fg(highligh_color)) .highlight_symbol(highligh_symbol) .style(Style::default().fg(Color::DarkGray)); frame.render_stateful_widget(word_key_list, words_layout[0], &mut self.list_state); if let Some(w) = selected_word.clone() { 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 = { if self.state == UiState::DELETE && let Some(s_w) = selected_word.clone() { Text::raw(format!("Sure you want to delete '{0}'? (y)es", s_w.key)) .style(Style::new().fg(Color::LightRed)) } else { 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( Constraint::Length(help_text.width() as u16), Constraint::Length(1), ); frame.render_widget(help_text, help_area); /* * State block * */ // let state_layout = Layout::horizontal(vec![Constraint::Fill(1)]) // .flex(Flex::Center) // .split(main_layout[2]); // frame.render_widget(help_text, help_area); } 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 { self.file_database .find_by_word_and_lang(&w_filter, &l_filter) } else if let Some(w_filter) = &self.word_filter { self.file_database.find_by_word(&w_filter) } else if let Some(l_filter) = &self.lang_filter { self.file_database.find_by_lang(&l_filter) } else { self.file_database.get_all_words() } }; self.list_state.select(Some(0)); self.description_scroll = 0; self.word_list = word_list; } fn format_ui_block_title(&self, input: &str, matching_state: UiState) -> String { if matching_state == self.state { let mut result = input.to_string(); result.push_str(" ● "); result } else { input.to_string() } } fn format_add_block_title(&self, input: &str, matching_state: AddState) -> String { if matching_state == self.add_state { let mut result = input.to_string(); result.push_str(" ● "); result } else { input.to_string() } } pub fn get_selected_word(&self) -> Option { let scroll_position: usize = { if let Some(i) = self.list_state.selected() { i } else { 0 } }; self.word_list.clone().into_iter().nth(scroll_position) } fn handle_events(&mut self) -> io::Result<()> { return handle_user_input(self); } }