Files
wordsmith/src/modules/ui/app.rs
2026-08-06 23:19:00 +02:00

335 lines
10 KiB
Rust

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<Word>,
pub word_to_add: Option<Word>,
pub word_to_edit: Option<Word>,
pub word_filter: Option<String>,
pub lang_filter: Option<String>,
pub last_key: Option<KeyEvent>,
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<ListItem> = 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<Word> {
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);
}
}