diff --git a/Cargo.toml b/Cargo.toml index 5451aaa..875df8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,4 +5,5 @@ edition = "2024" [dependencies] chrono = "0.4.42" +ratatui = "0.29.0" sqlite = "0.37.0" diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..9863147 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,29 @@ +use sqlite::Connection; + +pub enum CurrentScreen { + Main, + Exiting, +} + +pub struct App { + pub current_screen: CurrentScreen, + connection: Connection, + exit: bool, +} + +impl App { + pub fn new() -> App { + let connection = match Connection::open("ft_rs.db") { + Ok(con) => con, + Err(e) => { + eprintln!("Error opening database: {}", e); + panic!("stopping"); + } + }; + return App { + current_screen: CurrentScreen::Main, + connection: connection, + exit: false, + }; + } +} diff --git a/src/main.rs b/src/main.rs index fd2a14d..4c215de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,150 +1,214 @@ +mod app; mod data_layer; mod entities; -use chrono::{Local, TimeZone}; -use sqlite::Connection; -use std::io; +mod ui; +use crate::{ + app::{App, CurrentScreen}, + ui::ui, +}; +use ratatui::{ + Terminal, + crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, + }, + prelude::Backend, +}; +use std::{error::Error, io}; -use crate::entities::{Account, Transaction, TransactionType}; +fn main() -> Result<(), Box> { + enable_raw_mode()?; + let mut stderr = io::stderr(); + execute!(stderr, EnterAlternateScreen, EnableMouseCapture)?; -fn main() { - let connection = match Connection::open("ft_rs.db") { - Ok(con) => con, - Err(e) => { - eprintln!("Error opening database: {}", e); - return; - } - }; + let mut terminal = ratatui::init(); + let mut app = App::new(); + let res = run_app(&mut terminal, &mut app); - data_layer::setup(&connection); + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + if let Err(err) = res { + println!("{err:?}"); + } + return Ok(()); +} + +fn run_app(terminal: &mut Terminal, app: &mut App) -> io::Result { loop { - println!("Please choose an option:"); - println!("1. List accounts"); - println!("2. Add an account"); - println!("3. List transaction types"); - println!("4. Add a transaction type"); - println!("5. List transactions"); - println!("6. Add a transaction"); - println!("0. Exit"); + terminal.draw(|f| ui(f, app))?; - let mut choice = String::new(); - io::stdin() - .read_line(&mut choice) - .expect("Failed to read line"); + // S'occupe des keyevents + if let Event::Key(key) = event::read()? { + if key.kind == event::KeyEventKind::Release { + continue; + } - let choice = choice.trim(); - - match choice { - "1" => { - let accounts = data_layer::get_accounts(&connection); - let mut total = 0.0; - for ac in accounts.iter() { - let ac_total = ac.get_total(&connection); - println!( - "Id: {}, Name: {}, Total: {}", - ac.get_id(), - ac.get_name(), - ac_total - ); - total += ac_total; - } - println!("Total: {}", total); - } - "2" => { - println!("Please enter the account name:"); - let mut ac_name = String::new(); - io::stdin() - .read_line(&mut ac_name) - .expect("Failed to read line"); - let ac_name = ac_name.trim(); - let new_ac = Account::new(0, ac_name.to_string(), entities::AccountType::Cash); - data_layer::upsert_account(&connection, new_ac); - } - "3" => { - let types = data_layer::get_transaction_types(&connection); - for t in types.iter() { - println!("Name: {}", t.get_description()); - } - } - "4" => { - println!("Please enter the transaction type:"); - let mut desc = String::new(); - io::stdin() - .read_line(&mut desc) - .expect("Failed to read line"); - let desc = desc.trim(); - let tr_type = TransactionType::new(0, desc.to_string()); - data_layer::upsert_transaction_type(&connection, tr_type); - } - "5" => { - println!("Please enter the account id:"); - let mut ac_id_str = String::new(); - io::stdin() - .read_line(&mut ac_id_str) - .expect("Failed to read line"); - ac_id_str = ac_id_str.trim().to_string(); - let ac_id = ac_id_str.parse::().unwrap(); - let trx: Vec = - data_layer::get_account_transactions(&connection, ac_id); - // .sort_by(|a, b| b.get_date().cmp(&a.get_date())) - // .try_into() - // .unwrap(); - for t in trx.iter() { - println!( - "Date: {}, Type: {}, Description: {}, Amount: {}$", - Local.from_utc_datetime(&t.get_date().naive_local()), - t.get_type().get_description(), - t.get_desc(), - t.get_amount() - ); - } - } - "6" => { - println!("Please enter the account id:"); - let mut ac_id_str = String::new(); - io::stdin() - .read_line(&mut ac_id_str) - .expect("Failed to read line"); - ac_id_str = ac_id_str.trim().to_string(); - let ac_id = ac_id_str.parse::().unwrap(); - println!("Please enter the transaction type id:"); - let mut tr_type_id_str = String::new(); - io::stdin() - .read_line(&mut tr_type_id_str) - .expect("Failed to read line"); - tr_type_id_str = tr_type_id_str.trim().to_string(); - let type_id = tr_type_id_str.parse::().unwrap(); - println!("Please enter the amount:"); - let mut amnt_str = String::new(); - io::stdin() - .read_line(&mut amnt_str) - .expect("Failed to read line"); - amnt_str = amnt_str.trim().to_string(); - let amount = amnt_str.parse::().unwrap(); - println!("Please enter the description:"); - let mut desc = String::new(); - io::stdin() - .read_line(&mut desc) - .expect("Failed to read line"); - desc = desc.trim().to_string(); - let tr = Transaction::new( - 0, - ac_id, - amount, - chrono::offset::Utc::now(), - desc, - type_id, - &connection, - ); - data_layer::upsert_transaction(&connection, tr); - } - "0" => { - println!("Exiting..."); - break; // Exit the loop - } - _ => { - println!("Invalid choice."); + match app.current_screen { + CurrentScreen::Main => match key.code { + KeyCode::Char('q') => { + app.current_screen = CurrentScreen::Exiting; + } + _ => {} + }, + CurrentScreen::Exiting => match key.code { + KeyCode::Char('y') => { + return Ok(true); + } + _ => { + app.current_screen = CurrentScreen::Main; + } + }, } } } } + +// fn main() { +// let connection = match Connection::open("ft_rs.db") { +// Ok(con) => con, +// Err(e) => { +// eprintln!("Error opening database: {}", e); +// return; +// } +// }; + +// data_layer::setup(&connection); + +// loop { +// println!("Please choose an option:"); +// println!("1. List accounts"); +// println!("2. Add an account"); +// println!("3. List transaction types"); +// println!("4. Add a transaction type"); +// println!("5. List transactions"); +// println!("6. Add a transaction"); +// println!("0. Exit"); + +// let mut choice = String::new(); +// io::stdin() +// .read_line(&mut choice) +// .expect("Failed to read line"); + +// let choice = choice.trim(); + +// match choice { +// "1" => { +// let accounts = data_layer::get_accounts(&connection); +// let mut total = 0.0; +// for ac in accounts.iter() { +// let ac_total = ac.get_total(&connection); +// println!( +// "Id: {}, Name: {}, Total: {}", +// ac.get_id(), +// ac.get_name(), +// ac_total +// ); +// total += ac_total; +// } +// println!("Total: {}", total); +// } +// "2" => { +// println!("Please enter the account name:"); +// let mut ac_name = String::new(); +// io::stdin() +// .read_line(&mut ac_name) +// .expect("Failed to read line"); +// let ac_name = ac_name.trim(); +// let new_ac = Account::new(0, ac_name.to_string(), entities::AccountType::Cash); +// data_layer::upsert_account(&connection, new_ac); +// } +// "3" => { +// let types = data_layer::get_transaction_types(&connection); +// for t in types.iter() { +// println!("Name: {}", t.get_description()); +// } +// } +// "4" => { +// println!("Please enter the transaction type:"); +// let mut desc = String::new(); +// io::stdin() +// .read_line(&mut desc) +// .expect("Failed to read line"); +// let desc = desc.trim(); +// let tr_type = TransactionType::new(0, desc.to_string()); +// data_layer::upsert_transaction_type(&connection, tr_type); +// } +// "5" => { +// println!("Please enter the account id:"); +// let mut ac_id_str = String::new(); +// io::stdin() +// .read_line(&mut ac_id_str) +// .expect("Failed to read line"); +// ac_id_str = ac_id_str.trim().to_string(); +// let ac_id = ac_id_str.parse::().unwrap(); +// let trx: Vec = +// data_layer::get_account_transactions(&connection, ac_id); +// // .sort_by(|a, b| b.get_date().cmp(&a.get_date())) +// // .try_into() +// // .unwrap(); +// for t in trx.iter() { +// println!( +// "Date: {}, Type: {}, Description: {}, Amount: {}$", +// Local.from_utc_datetime(&t.get_date().naive_local()), +// t.get_type().get_description(), +// t.get_desc(), +// t.get_amount() +// ); +// } +// } +// "6" => { +// println!("Please enter the account id:"); +// let mut ac_id_str = String::new(); +// io::stdin() +// .read_line(&mut ac_id_str) +// .expect("Failed to read line"); +// ac_id_str = ac_id_str.trim().to_string(); +// let ac_id = ac_id_str.parse::().unwrap(); +// println!("Please enter the transaction type id:"); +// let mut tr_type_id_str = String::new(); +// io::stdin() +// .read_line(&mut tr_type_id_str) +// .expect("Failed to read line"); +// tr_type_id_str = tr_type_id_str.trim().to_string(); +// let type_id = tr_type_id_str.parse::().unwrap(); +// println!("Please enter the amount:"); +// let mut amnt_str = String::new(); +// io::stdin() +// .read_line(&mut amnt_str) +// .expect("Failed to read line"); +// amnt_str = amnt_str.trim().to_string(); +// let amount = amnt_str.parse::().unwrap(); +// println!("Please enter the description:"); +// let mut desc = String::new(); +// io::stdin() +// .read_line(&mut desc) +// .expect("Failed to read line"); +// desc = desc.trim().to_string(); +// let tr = Transaction::new( +// 0, +// ac_id, +// amount, +// chrono::offset::Utc::now(), +// desc, +// type_id, +// &connection, +// ); +// data_layer::upsert_transaction(&connection, tr); +// } +// "0" => { +// println!("Exiting..."); +// break; // Exit the loop +// } +// _ => { +// println!("Invalid choice."); +// } +// } +// } +// } diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..07e0c0e --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,51 @@ +use crate::app::{App, CurrentScreen}; +use ratatui::{ + Frame, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Style}, + text::Text, + widgets::{Block, Borders, Clear, Paragraph, Wrap}, +}; + +pub fn ui(frame: &mut Frame, app: &App) { + if let CurrentScreen::Exiting = app.current_screen { + frame.render_widget(Clear, frame.area()); + + let popup = Block::default() + .title("Exiting program") + .borders(Borders::all()) + .style(Style::default()); + + let exit_text = Text::styled( + "Are you sure you want to close the program? (y/n)", + Style::default().fg(Color::Red), + ); + + let exit_paragraph = Paragraph::new(exit_text) + .block(popup) + .wrap(Wrap { trim: false }); + + let area = centered_rect(60, 20, frame.area()); + frame.render_widget(exit_paragraph, area); + } +} + +fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage((100 - percent_y) / 2), + Constraint::Percentage(percent_y), + Constraint::Percentage((100 - percent_y) / 2), + ]) + .split(r); + + return Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage((100 - percent_x) / 2), + Constraint::Percentage(percent_x), + Constraint::Percentage((100 - percent_x) / 2), + ]) + .split(popup_layout[1])[1]; +}