database done + popup start
This commit is contained in:
commit
234b17a8b0
12 changed files with 1134 additions and 0 deletions
178
src/app.rs
Normal file
178
src/app.rs
Normal file
|
|
@ -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<Vec<AppEvent>> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
156
src/database.rs
Normal file
156
src/database.rs
Normal file
|
|
@ -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<NaiveDate, Vec<(u64, String)>> = TableDefinition::new("main"); // Vec<tag_id, event_description>
|
||||
const TAG_TABLE: TableDefinition<u64, String> = TableDefinition::new("tags");
|
||||
|
||||
pub struct DB {
|
||||
db: Database,
|
||||
}
|
||||
|
||||
impl DB {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
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<Option<DayInfo>, 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<Vec<(u64, String)>, 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<Option<String>, 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<u64, Error> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
22
src/day_info.rs
Normal file
22
src/day_info.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#[derive(Default, Clone)]
|
||||
pub struct DayInfo {
|
||||
pub events: Vec<Event>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct Event {
|
||||
pub tag: Option<u64>,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
impl Event {
|
||||
pub fn new(tag: Option<u64>, description: String) -> Self {
|
||||
Self { tag, description }
|
||||
}
|
||||
}
|
||||
|
||||
impl DayInfo {
|
||||
pub fn new(events: Vec<Event>) -> Self {
|
||||
Self { events }
|
||||
}
|
||||
}
|
||||
295
src/days.rs
Normal file
295
src/days.rs
Normal file
|
|
@ -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<Day>,
|
||||
state: DaysState,
|
||||
focused: bool,
|
||||
|
||||
selected_day: NaiveDate,
|
||||
pivot: Option<NaiveDate>,
|
||||
|
||||
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<Vec<AppEvent>> {
|
||||
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<Vec<AppEvent>> {
|
||||
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<NaiveDate>, 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<Vec<AppEvent>> {
|
||||
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<Vec<AppEvent>> {
|
||||
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<Constraint> =
|
||||
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<NaiveDate>,
|
||||
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
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
20
src/events.rs
Normal file
20
src/events.rs
Normal file
|
|
@ -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,
|
||||
}
|
||||
10
src/focused.rs
Normal file
10
src/focused.rs
Normal file
|
|
@ -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<Vec<AppEvent>>;
|
||||
}
|
||||
0
src/global_shortcuts.rs
Normal file
0
src/global_shortcuts.rs
Normal file
20
src/joiner.rs
Normal file
20
src/joiner.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
pub fn join_ascii_chars_functional(chars: &[&str]) -> String {
|
||||
let char_lines: Vec<Vec<&str>> = 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::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
25
src/main.rs
Normal file
25
src/main.rs
Normal file
|
|
@ -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<String> = args().collect();
|
||||
|
||||
let app_result = App::new().run(&mut terminal);
|
||||
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
146
src/month.rs
Normal file
146
src/month.rs
Normal file
|
|
@ -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<Vec<AppEvent>> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
68
src/popup.rs
Normal file
68
src/popup.rs
Normal file
|
|
@ -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<Vec<AppEvent>> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
194
src/year.rs
Normal file
194
src/year.rs
Normal file
|
|
@ -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<Vec<AppEvent>> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue