commit 234b17a8b042f3eef9e4df475034d9a56074ad0e Author: maxstrb Date: Sun Sep 14 12:13:04 2025 +0200 database done + popup start diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..3c3450b --- /dev/null +++ b/src/app.rs @@ -0,0 +1,178 @@ +use std::io; + +use chrono::{Datelike, Local}; +use crossterm::event::{self, Event, KeyEvent, KeyEventKind}; +use ratatui::{ + DefaultTerminal, Frame, + layout::{Constraint, Direction, Layout, Rect}, +}; + +use crate::{ + database::DB, days::Days, events::AppEvent, focused::Focused, month::Month, popup::Popup, + year::Year, +}; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum FocusedComponent { + Year, + Month, + Days, + DayPopup, +} + +pub struct App { + year: Year, + month: Month, + days: Days, + day_popup: Popup, + + focused: FocusedComponent, + exit: bool, +} + +impl App { + pub fn new() -> Self { + let mut app = App { + year: Year::default(), + month: Month::default(), + days: Days::default(), + day_popup: Popup::default(), + + focused: FocusedComponent::DayPopup, + exit: false, + }; + + app.days + .reload(app.month.month, app.year.year, Local::now().day() as i32); + + app.get_focused_mut().take_focus(); + app + } + + fn get_focused_mut(&mut self) -> &mut dyn Focused { + match self.focused { + FocusedComponent::Year => &mut self.year, + FocusedComponent::Month => &mut self.month, + FocusedComponent::Days => &mut self.days, + FocusedComponent::DayPopup => &mut self.day_popup, + } + } + + pub fn switch_focus(&mut self, new_focus: FocusedComponent) { + if self.focused != new_focus { + self.get_focused_mut().lose_focus(); + self.focused = new_focus; + self.get_focused_mut().take_focus(); + } + } + + fn handle_input(&mut self, key_event: KeyEvent, db: &mut DB) -> Option> { + let focused = self.get_focused_mut(); + focused.handle_input(key_event, db) + } + + pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> { + let mut db = DB::new().unwrap(); + + while !self.exit { + terminal.draw(|frame| self.draw(frame))?; + self.handle_events(&mut db)?; + } + Ok(()) + } + + fn draw(&mut self, frame: &mut Frame) { + let main_area = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![ + Constraint::Length(8), + Constraint::Length(4), + Constraint::Fill(1), + ]) + .split(frame.area()); + + let popup_area = Self::popup_rect(50, 50, frame.area()); + + if frame.area().width < 10 || frame.area().height < 30 { + self.exit(); + return; + } + + self.year.ready_to_render(main_area[0]); + self.month.ready_to_render(main_area[1]); + self.days.ready_to_render(main_area[2]); + self.day_popup.ready_to_render(popup_area); + + frame.render_widget(&self.year, main_area[0]); + frame.render_widget(&self.month, main_area[1]); + frame.render_widget(&self.days, main_area[2]); + + frame.render_widget(&self.day_popup, popup_area); + } + + fn popup_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); + + 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] + } + + fn exit(&mut self) { + self.exit = true; + } + + fn handle_events(&mut self, db: &mut DB) -> io::Result<()> { + match event::read()? { + Event::Key(key_event) if key_event.kind == KeyEventKind::Press => { + let app_event_option = self.handle_input(key_event, db); + + if let Some(app_event_vec) = app_event_option { + for app_event in app_event_vec { + match app_event { + AppEvent::SwitchFocus(new_focus) => self.switch_focus(new_focus), + AppEvent::Exit => self.exit(), + AppEvent::YearScrolled(dir) => match dir { + crate::events::Direction::Up => { + self.year.plus_year(); + } + crate::events::Direction::Down => { + self.year.minus_year(); + } + }, + AppEvent::MonthScrolled(dir) => match dir { + crate::events::Direction::Up => { + self.month.plus_month(); + } + crate::events::Direction::Down => { + self.month.minus_month(); + } + }, + AppEvent::MonthSet(month) => { + self.days.reload(month, self.year.year, 1); + } + AppEvent::YearSet(year) => { + self.days.reload(self.month.month, year, 1); + } + _ => (), + } + } + } + } + _ => {} + }; + Ok(()) + } +} diff --git a/src/database.rs b/src/database.rs new file mode 100644 index 0000000..a73eeb1 --- /dev/null +++ b/src/database.rs @@ -0,0 +1,156 @@ +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; +use std::time::{SystemTime, UNIX_EPOCH}; +use std::{env::home_dir, fs}; + +use crate::day_info::{DayInfo, Event}; +use chrono::NaiveDate; +use redb::{Database, Error, ReadableDatabase, ReadableTable, TableDefinition}; + +const MAIN_TABLE: TableDefinition> = TableDefinition::new("main"); // Vec +const TAG_TABLE: TableDefinition = TableDefinition::new("tags"); + +pub struct DB { + db: Database, +} + +impl DB { + pub fn new() -> Result { + let mut path = home_dir().ok_or(Error::DatabaseAlreadyOpen)?; + path.push(".local/share/cars"); + + fs::create_dir_all(&path)?; + + path.push("main.redb"); + let db = Database::create(path)?; + + let write_txn = db.begin_write()?; + + { + let _main_table = write_txn.open_table(MAIN_TABLE)?; + let _tag_table = write_txn.open_table(TAG_TABLE)?; + } + + write_txn.commit()?; + + Ok(Self { db }) + } + + pub fn get_day(&self, day: NaiveDate) -> Result, Error> { + match self.db.begin_read()?.open_table(MAIN_TABLE)?.get(day)? { + None => Ok(None), + Some(data) => { + let events = data.value(); + let mut output = DayInfo::default(); + + for (tag_id, event_description) in events { + let tag_name = self.db.begin_read()?.open_table(TAG_TABLE)?.get(tag_id)?; + output.events.push(match tag_name { + Some(_name) => Event::new(Some(tag_id), event_description), + + None => Event::new(None, event_description), + }); + } + + Ok(Some(output)) + } + } + } + + pub fn add_event(&mut self, day: NaiveDate, event: Event) -> Result<(), Error> { + let mut new_vec = match self.db.begin_read()?.open_table(MAIN_TABLE)?.get(day)? { + Some(d) => d.value(), + None => vec![], + }; + + new_vec.push(( + event.tag.expect("You should pass a valid event"), + event.description, + )); + + let write_txn = self.db.begin_write()?; + { + let mut table = write_txn.open_table(MAIN_TABLE)?; + table.insert(day, new_vec)?; + } + write_txn.commit()?; + + Ok(()) + } + + pub fn remove_event(&mut self, day: NaiveDate, index: usize) -> Result<(), Error> { + let mut old_vec = match self.db.begin_read()?.open_table(MAIN_TABLE)?.get(day)? { + Some(d) => d.value(), + None => vec![], + }; + + if old_vec.len() <= index { + return Ok(()); + } + + old_vec.remove(index); + + let write_txn = self.db.begin_write()?; + { + let mut table = write_txn.open_table(MAIN_TABLE)?; + table.insert(day, old_vec)?; + } + write_txn.commit()?; + + Ok(()) + } + + pub fn get_tags(&self) -> Result, Error> { + self.db + .begin_read()? + .open_table(TAG_TABLE)? + .iter()? + .map(|x| -> Result<(u64, String), Error> { + let val = x?; + Ok((val.0.value(), val.1.value())) + }) + .collect() + } + + pub fn get_tag_name(&self, id: u64) -> Result, Error> { + match self.db.begin_read()?.open_table(TAG_TABLE)?.get(id)? { + Some(name) => Ok(Some(name.value())), + None => Ok(None), + } + } + + pub fn create_tag(&mut self, tag_name: String) -> Result { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as u64; + + let mut hasher = DefaultHasher::new(); + tag_name.hash(&mut hasher); + let tag_hash = hasher.finish(); + + let tag_id = now.wrapping_add(tag_hash); + + let write_txn = self.db.begin_write()?; + { + let mut tag_table = write_txn.open_table(TAG_TABLE)?; + tag_table.insert(tag_id, tag_name)?; + } + write_txn.commit()?; + + std::thread::sleep(std::time::Duration::from_micros(1)); + + Ok(tag_id) + } + + pub fn delete_tag(&mut self, tag_id: u64) -> Result<(), Error> { + let write_txn = self.db.begin_write()?; + { + let mut table = write_txn.open_table(TAG_TABLE)?; + table.remove(tag_id)?; + } + write_txn.commit()?; + + Ok(()) + } +} diff --git a/src/day_info.rs b/src/day_info.rs new file mode 100644 index 0000000..e27e866 --- /dev/null +++ b/src/day_info.rs @@ -0,0 +1,22 @@ +#[derive(Default, Clone)] +pub struct DayInfo { + pub events: Vec, +} + +#[derive(Default, Clone)] +pub struct Event { + pub tag: Option, + pub description: String, +} + +impl Event { + pub fn new(tag: Option, description: String) -> Self { + Self { tag, description } + } +} + +impl DayInfo { + pub fn new(events: Vec) -> Self { + Self { events } + } +} diff --git a/src/days.rs b/src/days.rs new file mode 100644 index 0000000..f055f73 --- /dev/null +++ b/src/days.rs @@ -0,0 +1,295 @@ +use crate::{ + app::FocusedComponent, database::DB, day_info::DayInfo, events::AppEvent, focused::Focused, +}; +use chrono::{Datelike, Duration, NaiveDate}; +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Layout, Margin, Rect}, + style::{Color, Style}, + symbols::border, + text::Line, + widgets::{Block, Widget}, +}; + +use std::collections::VecDeque; + +#[derive(Default)] +pub struct Days { + days: VecDeque, + state: DaysState, + focused: bool, + + selected_day: NaiveDate, + pivot: Option, + + date_of_first_day: i32, + + current_month: i32, + current_year: i32, +} + +#[derive(Default)] +enum DaysState { + #[default] + Main, + DaySelect, +} + +enum CursorMovement { + Up, + Down, +} + +impl Days { + pub fn ready_to_render(&mut self, area: Rect) { + let view_rect = area.inner(Margin::new(1, 1)); + let number_of_days_shown = view_rect.height; + + if number_of_days_shown == 0 { + return; + } + + if self.days.is_empty() || self.days.len() > number_of_days_shown as usize { + self.reload( + (self.selected_day.month0() + 1) as i32, + self.selected_day.year_ce().1 as i32, + (self.selected_day.day0() + 1) as i32, + ); + } + + while self.days.len() != number_of_days_shown.into() { + let next_day = Day::new( + self.days + .iter() + .last() + .unwrap() + .day + .checked_add_days(chrono::Days::new(1)) + .unwrap(), + ); + if next_day.day.year() >= 10_000 { + break; + } + self.days.push_back(next_day); + } + + while self.days.len() != number_of_days_shown.into() { + let next_day = Day::new( + self.days + .iter() + .last() + .unwrap() + .day + .checked_sub_days(chrono::Days::new(1)) + .unwrap(), + ); + if next_day.day.year() < 0 { + break; + } + self.days.push_front(next_day); + } + } + + pub fn reload(&mut self, month: i32, year: i32, day: i32) { + self.current_month = month; + self.current_year = year; + + self.days.clear(); + self.days.push_front(Day::new( + NaiveDate::from_ymd_opt(year, month as u32, day as u32).unwrap(), + )); + self.selected_day = self.days.iter().next().unwrap().day; + self.date_of_first_day = self.days.iter().next().unwrap().index(); + } + + fn handle_main_state_input(&mut self, key_code: KeyCode) -> Option> { + match key_code { + KeyCode::Esc => Some(vec![AppEvent::Exit]), + KeyCode::Enter => { + self.state = DaysState::DaySelect; + None + } + KeyCode::Up => Some(vec![AppEvent::SwitchFocus(FocusedComponent::Month)]), + KeyCode::Down => { + self.state = DaysState::DaySelect; + self.handle_focused_arrows(CursorMovement::Down) + } + _ => None, + } + } + + fn handle_focused_arrows(&mut self, cursor: CursorMovement) -> Option> { + match cursor { + CursorMovement::Up => { + if self.selected_day == NaiveDate::from_ymd_opt(9999, 12, 31).unwrap() { + return None; + } + } + CursorMovement::Down => { + if self.selected_day == NaiveDate::from_ymd_opt(0, 1, 1).unwrap() { + return None; + } + } + } + + let mut events = vec![]; + let previous_day = self.selected_day; + + self.selected_day += match cursor { + CursorMovement::Up => Duration::days(-1), + CursorMovement::Down => Duration::days(1), + }; + + if self.selected_day < self.days.iter().next().unwrap().day { + self.days.pop_back(); + self.days.push_front(Day::new(self.selected_day)); + } + + if self.selected_day > self.days.iter().last().unwrap().day { + self.days.pop_front(); + self.days.push_back(Day::new(self.selected_day)); + } + + if previous_day.month0() != self.selected_day.month0() { + events.push(AppEvent::MonthScrolled(match cursor { + CursorMovement::Up => crate::events::Direction::Down, + CursorMovement::Down => crate::events::Direction::Up, + })); + } + + if previous_day.year_ce().1 != self.selected_day.year_ce().1 { + events.push(AppEvent::YearScrolled(match cursor { + CursorMovement::Up => crate::events::Direction::Down, + CursorMovement::Down => crate::events::Direction::Up, + })); + } + + if events.is_empty() { + None + } else { + Some(events) + } + } + + fn is_date_between(start: NaiveDate, end: Option, current: NaiveDate) -> bool { + if let Some(e) = end { + (start <= current && current <= e) || (e <= current && current <= start) + } else { + current == start + } + } + + fn handle_day_select_state_input(&mut self, key_code: KeyCode) -> Option> { + match key_code { + KeyCode::Esc => { + self.state = DaysState::Main; + None + } + KeyCode::Enter => None, + KeyCode::Down => self.handle_focused_arrows(CursorMovement::Down), + KeyCode::Up => self.handle_focused_arrows(CursorMovement::Up), + KeyCode::Char(' ') => { + self.pivot = match self.pivot { + None => Some(self.selected_day), + Some(_) => None, + }; + None + } + _ => None, + } + } +} + +impl Focused for Days { + fn take_focus(&mut self) { + self.focused = true; + } + + fn lose_focus(&mut self) { + self.focused = false; + } + + fn handle_input(&mut self, key_event: KeyEvent, _db: &mut DB) -> Option> { + match self.state { + DaysState::Main => self.handle_main_state_input(key_event.code), + DaysState::DaySelect => self.handle_day_select_state_input(key_event.code), + } + } +} + +impl Widget for &Days { + fn render(self, area: Rect, buf: &mut Buffer) { + let block = Block::bordered() + .border_set(border::ROUNDED) + .border_style(Style::default().fg(match self.focused { + true => Color::Blue, + false => Color::White, + })) + .title_alignment(ratatui::layout::Alignment::Center) + .title_bottom(" (T) to manage tags | (A) to add event | (Return) to show more "); + + let inner_area = block.inner(area); + + let constraints: Vec = + self.days.iter().map(|_| Constraint::Length(1)).collect(); + + if !constraints.is_empty() { + let day_areas = Layout::default() + .direction(ratatui::layout::Direction::Vertical) + .constraints(constraints) + .split(inner_area); + + for (day, area) in self.days.iter().zip(day_areas.iter()) { + day.get_line(self.selected_day, self.pivot, self.date_of_first_day) + .render(*area, buf); + } + } + + block.render(area, buf); + } +} + +#[derive(Clone)] +pub struct Day { + pub day: NaiveDate, + pub info: DayInfo, +} + +impl Day { + pub fn new(day: NaiveDate) -> Self { + Self { + day, + info: DayInfo::default(), + } + } + + fn index(&self) -> i32 { + self.day + .signed_duration_since(NaiveDate::from_ymd_opt(0, 1, 1).unwrap()) + .num_days() as i32 + } + + fn get_line( + &self, + current_day: NaiveDate, + pivot: Option, + date_of_first_day: i32, + ) -> Line<'_> { + Line::styled( + format!( + "{}, {}, {}", + self.day.day(), + self.day.month(), + self.day.year() + ), + Style::new().bg(if Days::is_date_between(current_day, pivot, self.day) { + Color::Red + } else if (self.index() - date_of_first_day).abs() % 2 == 0 { + Color::DarkGray + } else { + Color::Reset + }), + ) + } +} diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..be10199 --- /dev/null +++ b/src/events.rs @@ -0,0 +1,20 @@ +use crate::app::FocusedComponent; +use crate::day_info::DayInfo; + +pub enum AppEvent { + SwitchFocus(FocusedComponent), + DaySelected(DayInfo), + + YearSet(i32), + MonthSet(i32), + + YearScrolled(Direction), + MonthScrolled(Direction), + + Exit, +} + +pub enum Direction { + Up, + Down, +} diff --git a/src/focused.rs b/src/focused.rs new file mode 100644 index 0000000..21cbd05 --- /dev/null +++ b/src/focused.rs @@ -0,0 +1,10 @@ +use crossterm::event::KeyEvent; + +use crate::database::DB; +use crate::events::AppEvent; + +pub trait Focused { + fn take_focus(&mut self); + fn lose_focus(&mut self); + fn handle_input(&mut self, key_event: KeyEvent, db: &mut DB) -> Option>; +} diff --git a/src/global_shortcuts.rs b/src/global_shortcuts.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/joiner.rs b/src/joiner.rs new file mode 100644 index 0000000..413c29c --- /dev/null +++ b/src/joiner.rs @@ -0,0 +1,20 @@ +pub fn join_ascii_chars_functional(chars: &[&str]) -> String { + let char_lines: Vec> = chars.iter().map(|ch| ch.lines().collect()).collect(); + + if char_lines.is_empty() { + return String::new(); + } + + (0..char_lines[0].len()) + .map(|row| { + char_lines + .iter() + .filter_map(|char_line| char_line.get(row)) + .fold(String::new(), |mut acc, s| { + acc.push_str(s); + acc + }) + }) + .collect::>() + .join("\n") +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..166b180 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,25 @@ +use std::{/*env::args,*/ io}; + +mod app; +mod database; +mod day_info; +mod days; +mod events; +mod focused; +mod global_shortcuts; +mod joiner; +mod month; +mod popup; +mod year; + +use app::App; + +fn main() -> io::Result<()> { + let mut terminal = ratatui::init(); + //let command_line_args: Vec = args().collect(); + + let app_result = App::new().run(&mut terminal); + + ratatui::restore(); + app_result +} diff --git a/src/month.rs b/src/month.rs new file mode 100644 index 0000000..18c4780 --- /dev/null +++ b/src/month.rs @@ -0,0 +1,146 @@ +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + buffer::Buffer, + layout::Rect, + style::{Color, Style}, + symbols::border, + widgets::{Block, Paragraph, Widget}, +}; + +use crate::{ + app::FocusedComponent, + database::DB, + events::{AppEvent, Direction}, + focused::Focused, +}; + +use chrono::{Datelike, Local}; + +const MONTHS: &[&str] = &[ + "\ +░░▒█▒▄▀▄░█▄░█░█▒█▒▄▀▄▒█▀▄░▀▄▀ +░▀▄█░█▀█░█▒▀█░▀▄█░█▀█░█▀▄░▒█▒", + "\ +▒█▀▒██▀░██▄▒█▀▄░█▒█▒▄▀▄▒█▀▄░▀▄▀ +░█▀░█▄▄▒█▄█░█▀▄░▀▄█░█▀█░█▀▄░▒█▒", + "\ +░█▄▒▄█▒▄▀▄▒█▀▄░▄▀▀░█▄█ +░█▒▀▒█░█▀█░█▀▄░▀▄▄▒█▒█", + "\ +▒▄▀▄▒█▀▄▒█▀▄░█░█▒░ +░█▀█░█▀▒░█▀▄░█▒█▄▄", + "\ +░█▄▒▄█▒▄▀▄░▀▄▀ +░█▒▀▒█░█▀█░▒█▒", + "\ +░░▒█░█▒█░█▄░█▒██▀ +░▀▄█░▀▄█░█▒▀█░█▄▄", + "\ +░░▒█░█▒█░█▒░░▀▄▀ +░▀▄█░▀▄█▒█▄▄░▒█▒", + "\ +▒▄▀▄░█▒█░▄▀▒░█▒█░▄▀▀░▀█▀ +░█▀█░▀▄█░▀▄█░▀▄█▒▄██░▒█▒", + "\ +░▄▀▀▒██▀▒█▀▄░▀█▀▒██▀░█▄▒▄█░██▄▒██▀▒█▀▄ +▒▄██░█▄▄░█▀▒░▒█▒░█▄▄░█▒▀▒█▒█▄█░█▄▄░█▀▄", + "\ +░▄▀▄░▄▀▀░▀█▀░▄▀▄░██▄▒██▀▒█▀▄ +░▀▄▀░▀▄▄░▒█▒░▀▄▀▒█▄█░█▄▄░█▀▄", + "\ +░█▄░█░▄▀▄░█▒█▒██▀░█▄▒▄█░██▄▒██▀▒█▀▄ +░█▒▀█░▀▄▀░▀▄▀░█▄▄░█▒▀▒█▒█▄█░█▄▄░█▀▄", + "\ +░█▀▄▒██▀░▄▀▀▒██▀░█▄▒▄█░██▄▒██▀▒█▀▄ +▒█▄▀░█▄▄░▀▄▄░█▄▄░█▒▀▒█▒█▄█░█▄▄░█▀▄", +]; + +pub struct Month { + pub month: i32, + + focused: bool, +} + +impl Default for Month { + fn default() -> Self { + Self { + month: Local::now().month() as i32, + focused: false, + } + } +} + +impl Month { + pub fn ready_to_render(&mut self, _area: Rect) {} + pub fn plus_month(&mut self) { + self.month = (self.month % 12) + 1; + } + pub fn minus_month(&mut self) { + self.month = if self.month == 1 { 12 } else { self.month - 1 } + } +} + +impl Focused for Month { + fn take_focus(&mut self) { + self.focused = true; + } + + fn lose_focus(&mut self) { + self.focused = false; + } + + fn handle_input(&mut self, key_event: KeyEvent, _db: &mut DB) -> Option> { + match key_event.code { + KeyCode::Esc => Some(vec![AppEvent::Exit]), + KeyCode::Up => Some(vec![AppEvent::SwitchFocus(FocusedComponent::Year)]), + KeyCode::Down => Some(vec![AppEvent::SwitchFocus(FocusedComponent::Days)]), + KeyCode::Right => { + self.month += 1; + + if self.month >= 13 { + self.month = 1; + + Some(vec![ + AppEvent::YearScrolled(Direction::Up), + AppEvent::MonthSet(self.month), + ]) + } else { + Some(vec![AppEvent::MonthSet(self.month)]) + } + } + KeyCode::Left => { + self.month -= 1; + + if self.month <= 0 { + self.month = 12; + + Some(vec![ + AppEvent::YearScrolled(Direction::Down), + AppEvent::MonthSet(self.month), + ]) + } else { + Some(vec![AppEvent::MonthSet(self.month)]) + } + } + _ => None, + } + } +} + +impl Widget for &Month { + fn render(self, area: Rect, buf: &mut Buffer) { + let block = + Block::bordered() + .border_set(border::ROUNDED) + .border_style(Style::default().fg(match self.focused { + true => Color::Blue, + false => Color::White, + })); + + let bordered_area = block.inner(area); + + block.render(area, buf); + + Paragraph::new(MONTHS[(self.month - 1) as usize]).render(bordered_area, buf); + } +} diff --git a/src/popup.rs b/src/popup.rs new file mode 100644 index 0000000..caea3ca --- /dev/null +++ b/src/popup.rs @@ -0,0 +1,68 @@ +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + buffer::Buffer, + layout::Rect, + style::{Color, Style}, + symbols::border, + widgets::{Block, Clear, Widget}, +}; + +use crate::{app::FocusedComponent, database::DB, database::*, events::AppEvent, focused::Focused}; + +pub enum PopupType { + TagManagement(Vec<(u64, String)>), + DayDetails, + EventAdding, +} + +impl Default for PopupType { + fn default() -> Self { + PopupType::TagManagement(vec![]) + } +} + +#[derive(Default)] +pub struct Popup { + self_state: PopupType, + focused: bool, +} + +impl Popup { + pub fn ready_to_render(&mut self, _area: Rect) {} +} + +impl Focused for Popup { + fn take_focus(&mut self) { + self.focused = true; + } + + fn lose_focus(&mut self) { + self.focused = false; + } + + fn handle_input(&mut self, key_event: KeyEvent, _db: &mut DB) -> Option> { + match key_event.code { + KeyCode::Esc => Some(vec![AppEvent::SwitchFocus(FocusedComponent::Days)]), + _ => None, + } + } +} + +impl Widget for &Popup { + fn render(self, area: Rect, buf: &mut Buffer) { + if !self.focused { + return; + } + + Clear.render(area, buf); + + Block::bordered() + .border_set(border::ROUNDED) + .border_style(Style::default().fg(match self.focused { + true => Color::Blue, + false => Color::White, + })) + .title("DayPopup") + .render(area, buf); + } +} diff --git a/src/year.rs b/src/year.rs new file mode 100644 index 0000000..6016930 --- /dev/null +++ b/src/year.rs @@ -0,0 +1,194 @@ +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + buffer::Buffer, + layout::Rect, + style::{Color, Style}, + symbols::border, + widgets::{Block, Paragraph, Widget}, +}; + +use crate::{ + app::FocusedComponent, database::DB, events::AppEvent, focused::Focused, + joiner::join_ascii_chars_functional, +}; + +use chrono::{Datelike, Local}; + +const DIGITS: &[&str] = &[ + "\ +╔═══╗ +║╔═╗║ +║║ ║║ +║║ ║║ +║╚═╝║ +╚═══╝", + " ╔╗ +╔╝║ +╚╗║ + ║║ +╔╝╚╗ +╚══╝", + "\ +╔═══╗ +║╔═╗║ +╚╝╔╝║ +╔═╝╔╝ +║║╚═╗ +╚═══╝", + "\ +╔═══╗ +║╔═╗║ +╚╝╔╝║ +╔╗╚╗║ +║╚═╝║ +╚═══╝", + "\ +╔╗ ╔╗ +║║ ║║ +║╚═╝║ +╚══╗║ + ║║ + ╚╝", + "\ +╔═══╗ +║╔══╝ +║╚══╗ +╚══╗║ +╔══╝║ +╚═══╝", + "\ +╔═══╗ +║╔══╝ +║╚══╗ +║╔═╗║ +║╚═╝║ +╚═══╝", + "\ +╔═══╗ +║╔═╗║ +╚╝╔╝║ + ║╔╝ + ║║ + ╚╝ ", + "\ +╔═══╗ +║╔═╗║ +║╚═╝║ +║╔═╗║ +║╚═╝║ +╚═══╝", + "\ +╔═══╗ +║╔═╗║ +║╚═╝║ +╚══╗║ +╔══╝║ +╚═══╝", +]; + +pub struct Year { + pub year: i32, + focused: bool, +} + +impl Default for Year { + fn default() -> Self { + Self { + year: Local::now().year(), + focused: false, + } + } +} + +impl Year { + pub fn ready_to_render(&mut self, _area: Rect) {} + + pub fn month_overflow(&mut self) { + self.plus_year(); + } + + pub fn month_underflow(&mut self) { + self.minus_year(); + } + + pub fn plus_year(&mut self) -> bool { + if self.year < 10_000 { + self.year += 1; + true + } else { + false + } + } + + pub fn minus_year(&mut self) -> bool { + if self.year > 0 { + self.year -= 1; + true + } else { + false + } + } +} + +impl Focused for Year { + fn take_focus(&mut self) { + self.focused = true; + } + + fn lose_focus(&mut self) { + self.focused = false; + } + + fn handle_input(&mut self, key_event: KeyEvent, _db: &mut DB) -> Option> { + match key_event.code { + KeyCode::Esc => Some(vec![AppEvent::Exit]), + KeyCode::Down => Some(vec![AppEvent::SwitchFocus(FocusedComponent::Month)]), + KeyCode::Right => { + if self.plus_year() { + Some(vec![AppEvent::YearSet(self.year)]) + } else { + None + } + } + KeyCode::Left => { + if self.minus_year() { + Some(vec![AppEvent::YearSet(self.year)]) + } else { + None + } + } + _ => None, + } + } +} + +impl Widget for &Year { + fn render(self, area: Rect, buf: &mut Buffer) { + let block = + Block::bordered() + .border_set(border::ROUNDED) + .border_style(Style::default().fg(match self.focused { + true => Color::Blue, + false => Color::White, + })); + + let bordered_area = block.inner(area); + + block.render(area, buf); + + let digit_one = (self.year - self.year % 1000) / 1000; + let digit_two = (self.year - digit_one * 1000 - self.year % 100) / 100; + + let digit_four = self.year % 10; + let digit_three = ((self.year - digit_four) % 100) / 10; + + let chars = vec![ + DIGITS[digit_one as usize], + DIGITS[digit_two as usize], + DIGITS[digit_three as usize], + DIGITS[digit_four as usize], + ]; + + Paragraph::new(join_ascii_chars_functional(&chars)).render(bordered_area, buf); + } +}