use crate::app::events::EventHandler;
use crate::db::config::{Config, DBConfig};
use crate::db::connections::init_db;
use crate::db::models::{TodoList, UIList};
use crate::ui::components::{
AddDBPopUp, AddItemPopUp, AddListPopUp, ChangeDBPopUp, DBSelector, InputState, ItemsComponent,
ListsComponent, Logo, ModifyItemPopUp, ModifyListPopUp,
};
use crate::ui::cursor::CursorState;
use crate::ui::layout::AppLayout;
use anyhow::{Context, Result};
use crossterm::event::{self, KeyEvent};
use ratatui::DefaultTerminal;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::widgets::Widget;
use sqlx::SqlitePool;
#[derive(Debug, Clone, PartialEq)]
pub enum CurrentScreen {
Main,
AddList,
ModifyList,
AddItem,
ModifyItem,
ChangeDB,
AddDB,
}
pub struct App {
pub config: Config,
pub current_db_config: DBConfig,
pub current_screen: CurrentScreen,
pub pool: SqlitePool,
pub lists_component: ListsComponent,
pub input_state: InputState,
pub selected_db_index: usize,
pub exit: bool,
}
impl App {
pub async fn new() -> Self {
let config = Config::read().expect("Failed to read config file");
let default_db_config = config
.get_default()
.expect("Couldn't fetch default database");
let pool = init_db(&default_db_config.connection_str)
.await
.expect("Failed to connect to database");
let current_screen = CurrentScreen::Main;
let mut lists_component = ListsComponent::new();
lists_component
.load_lists(&pool)
.await
.expect("Failed to read lists");
Self {
config,
current_db_config: default_db_config,
current_screen,
pool,
lists_component,
input_state: InputState::new(),
selected_db_index: 0,
exit: false,
}
}
pub async fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> {
while !self.exit {
terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?;
if let Some(key) = event::read()?.as_key_press_event() {
self.handle_key_event(key).await;
}
}
Ok(())
}
pub async fn create_new_database(
&mut self,
db_name: String,
set_as_default: bool,
) -> Result<()> {
let data_dir = dirs::data_dir()
.ok_or_else(|| anyhow::anyhow!("Could not find data directory"))?
.join("judo");
std::fs::create_dir_all(&data_dir).with_context(|| "Failed to create data directory")?;
let db_file = format!("{}.db", db_name);
let path = data_dir.join(db_file);
let connection_str = format!("sqlite:{}", path.display());
let new_db_config = DBConfig {
name: db_name.clone(),
connection_str: connection_str.clone(),
};
init_db(&connection_str)
.await
.with_context(|| "Failed to initialize new database")?;
self.config.dbs.push(new_db_config);
if set_as_default {
self.config.default = db_name.clone();
}
let config_dir = dirs::config_dir()
.ok_or_else(|| anyhow::anyhow!("Could not find config directory"))?
.join("judo");
let config_path = config_dir.join("judo.toml");
self.config
.write(&config_path)
.with_context(|| "Failed to save config")?;
self.selected_db_index = self.config.dbs.len() - 1;
Ok(())
}
async fn handle_key_event(&mut self, key: KeyEvent) {
match self.current_screen {
CurrentScreen::Main => EventHandler::handle_main_screen_key(self, key).await,
CurrentScreen::AddList | CurrentScreen::ModifyList => {
EventHandler::handle_add_or_modify_list_screen_key(self, key).await
}
CurrentScreen::AddItem => {
EventHandler::handle_add_or_modify_item_screen_key(self, key).await
}
CurrentScreen::ModifyItem => {
EventHandler::handle_add_or_modify_item_screen_key(self, key).await
}
CurrentScreen::ChangeDB => EventHandler::handle_change_db_screen_key(self, key).await,
CurrentScreen::AddDB => EventHandler::handle_add_db_screen_key(self, key).await,
}
}
pub fn enter_add_list_screen(&mut self) {
self.input_state = InputState::default();
self.current_screen = CurrentScreen::AddList;
}
pub fn enter_modify_list_screen(&mut self, selected_list: &TodoList) {
self.input_state = InputState {
current_input: selected_list.name.clone(),
cursor_pos: 0,
is_modifying: true,
};
self.current_screen = CurrentScreen::ModifyList;
}
pub fn enter_add_item_screen(&mut self) {
if self.lists_component.selected().is_some() {
self.input_state = InputState::default();
self.current_screen = CurrentScreen::AddItem;
}
}
pub fn enter_modify_item_screen(&mut self, ui_list: &UIList) {
if self.lists_component.selected().is_some()
&& let Some(j) = ui_list.item_state.selected()
{
let selected_item = ui_list.items[j].item.clone();
self.input_state = InputState {
current_input: selected_item.name.clone(),
cursor_pos: 0,
is_modifying: true,
};
self.current_screen = CurrentScreen::ModifyItem;
}
}
pub fn exit_add_or_modify_list_without_saving(&mut self) {
self.current_screen = CurrentScreen::Main;
self.input_state.clear();
}
pub fn exit_add_item_without_saving(&mut self) {
self.current_screen = CurrentScreen::Main;
self.input_state.clear();
}
pub fn enter_change_db_screen(&mut self) {
self.selected_db_index = self
.config
.dbs
.iter()
.position(|db| db.name == self.current_db_config.name)
.unwrap_or(0);
self.current_screen = CurrentScreen::ChangeDB;
}
pub fn exit_change_db_without_saving(&mut self) {
self.current_screen = CurrentScreen::Main;
}
pub fn enter_add_db_screen(&mut self) {
self.current_screen = CurrentScreen::AddDB;
}
pub fn exit_add_db_without_saving(&mut self) {
self.current_screen = CurrentScreen::ChangeDB;
self.input_state.clear();
}
pub fn select_previous_db(&mut self) {
if self.config.dbs.is_empty() {
return;
}
self.selected_db_index = if self.selected_db_index == 0 {
self.config.dbs.len() - 1
} else {
self.selected_db_index - 1
};
}
pub fn select_next_db(&mut self) {
if self.config.dbs.is_empty() {
return;
}
self.selected_db_index = (self.selected_db_index + 1) % self.config.dbs.len();
}
pub async fn switch_to_selected_db(&mut self) -> Result<()> {
if let Some(selected_db) = self.config.dbs.get(self.selected_db_index) {
let new_pool = init_db(&selected_db.connection_str)
.await
.with_context(|| "Failed to connect to database")?;
self.current_db_config = selected_db.clone();
self.pool = new_pool;
self.lists_component = ListsComponent::new();
self.lists_component
.load_lists(&self.pool)
.await
.with_context(|| "Failed to load lists")?;
self.current_screen = CurrentScreen::Main;
}
Ok(())
}
pub async fn set_selected_db_as_default(&mut self) -> Result<()> {
if let Some(selected_db) = self.config.dbs.get(self.selected_db_index) {
self.config.default = selected_db.name.clone();
let config_dir = dirs::config_dir()
.ok_or_else(|| anyhow::anyhow!("Could not find config directory"))?
.join("judo");
let config_path = config_dir.join("judo.toml");
self.config
.write(&config_path)
.with_context(|| "Failed to save config")?;
}
Ok(())
}
}
impl Widget for &mut App {
fn render(self, area: Rect, buf: &mut Buffer) {
AppLayout::render_background(self.config.clone(), area, buf);
let (lists_area, items_area, logo_area, db_selector_area, closed_selector_area) =
AppLayout::calculate_main_layout(area);
Logo::render(logo_area, buf);
if !matches!(
self.current_screen,
CurrentScreen::ChangeDB | CurrentScreen::AddDB
) {
DBSelector::render(
closed_selector_area,
buf,
&self.current_db_config.name,
self.config.clone(),
);
}
self.lists_component
.render(lists_area, buf, self.config.clone());
let selected_list = self.lists_component.get_selected_list_mut();
ItemsComponent::render(selected_list, items_area, buf, self.config.clone());
match self.current_screen {
CurrentScreen::AddList => {
AddListPopUp::render(self.config.clone(), &self.input_state, lists_area, buf)
}
CurrentScreen::ModifyList => {
ModifyListPopUp::render(self.config.clone(), &self.input_state, lists_area, buf)
}
CurrentScreen::AddItem => {
AddItemPopUp::render(self.config.clone(), &self.input_state, items_area, buf)
}
CurrentScreen::ModifyItem => {
ModifyItemPopUp::render(self.config.clone(), &self.input_state, items_area, buf)
}
CurrentScreen::ChangeDB => {
ChangeDBPopUp::render(&self.config, self.selected_db_index, db_selector_area, buf)
}
CurrentScreen::AddDB => AddDBPopUp::render(
self.config.clone(),
&self.input_state,
db_selector_area,
buf,
),
_ => {}
}
}
}