tui done
This commit is contained in:
parent
234b17a8b0
commit
3681aa2a22
15 changed files with 1109 additions and 143 deletions
49
src/app.rs
49
src/app.rs
|
|
@ -1,9 +1,9 @@
|
|||
use std::io;
|
||||
|
||||
use chrono::{Datelike, Local};
|
||||
use crossterm::event::{self, Event, KeyEvent, KeyEventKind};
|
||||
use ratatui::{
|
||||
DefaultTerminal, Frame,
|
||||
crossterm::event::{self, KeyEvent, KeyEventKind},
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
};
|
||||
|
||||
|
|
@ -20,17 +20,17 @@ pub enum FocusedComponent {
|
|||
DayPopup,
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
pub struct App<'a> {
|
||||
year: Year,
|
||||
month: Month,
|
||||
days: Days,
|
||||
day_popup: Popup,
|
||||
day_popup: Popup<'a>,
|
||||
|
||||
focused: FocusedComponent,
|
||||
exit: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
impl App<'_> {
|
||||
pub fn new() -> Self {
|
||||
let mut app = App {
|
||||
year: Year::default(),
|
||||
|
|
@ -38,7 +38,7 @@ impl App {
|
|||
days: Days::default(),
|
||||
day_popup: Popup::default(),
|
||||
|
||||
focused: FocusedComponent::DayPopup,
|
||||
focused: FocusedComponent::Days,
|
||||
exit: false,
|
||||
};
|
||||
|
||||
|
|
@ -75,13 +75,13 @@ impl App {
|
|||
let mut db = DB::new().unwrap();
|
||||
|
||||
while !self.exit {
|
||||
terminal.draw(|frame| self.draw(frame))?;
|
||||
terminal.draw(|frame| self.draw(frame, &mut db))?;
|
||||
self.handle_events(&mut db)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame) {
|
||||
fn draw(&mut self, frame: &mut Frame, db: &mut DB) {
|
||||
let main_area = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![
|
||||
|
|
@ -100,7 +100,7 @@ impl App {
|
|||
|
||||
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.days.ready_to_render(main_area[2], db);
|
||||
self.day_popup.ready_to_render(popup_area);
|
||||
|
||||
frame.render_widget(&self.year, main_area[0]);
|
||||
|
|
@ -136,7 +136,9 @@ impl App {
|
|||
|
||||
fn handle_events(&mut self, db: &mut DB) -> io::Result<()> {
|
||||
match event::read()? {
|
||||
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
|
||||
ratatui::crossterm::event::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 {
|
||||
|
|
@ -160,13 +162,40 @@ impl App {
|
|||
self.month.minus_month();
|
||||
}
|
||||
},
|
||||
AppEvent::Reload => {
|
||||
self.days.reload_day_counts(db);
|
||||
}
|
||||
AppEvent::MonthSet(month) => {
|
||||
self.days.reload(month, self.year.year, 1);
|
||||
}
|
||||
AppEvent::YearSet(year) => {
|
||||
self.days.reload(self.month.month, year, 1);
|
||||
}
|
||||
_ => (),
|
||||
|
||||
AppEvent::AddEvent(start, end) => {
|
||||
self.switch_focus(FocusedComponent::DayPopup);
|
||||
self.day_popup.add_event(
|
||||
start,
|
||||
end,
|
||||
db.get_tags().expect("Database error"),
|
||||
);
|
||||
}
|
||||
|
||||
AppEvent::DaySelected(day) => {
|
||||
self.switch_focus(FocusedComponent::DayPopup);
|
||||
let events =
|
||||
db.get_day(day).expect("Database error").unwrap_or_default();
|
||||
let tag_names = events
|
||||
.iter()
|
||||
.map(|e| match e.tag {
|
||||
Some(id) => {
|
||||
db.get_tag_name(id).expect("Database error").unwrap()
|
||||
}
|
||||
None => "Tag deleted".to_owned(),
|
||||
})
|
||||
.collect();
|
||||
self.day_popup.show_day(day, events, tag_names);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::hash::{Hash, Hasher};
|
|||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use std::{env::home_dir, fs};
|
||||
|
||||
use crate::day_info::{DayInfo, Event};
|
||||
use crate::day_info::Event;
|
||||
use chrono::NaiveDate;
|
||||
use redb::{Database, Error, ReadableDatabase, ReadableTable, TableDefinition};
|
||||
|
||||
|
|
@ -36,16 +36,16 @@ impl DB {
|
|||
Ok(Self { db })
|
||||
}
|
||||
|
||||
pub fn get_day(&self, day: NaiveDate) -> Result<Option<DayInfo>, Error> {
|
||||
pub fn get_day(&self, day: NaiveDate) -> Result<Option<Vec<Event>>, 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();
|
||||
let mut output = vec![];
|
||||
|
||||
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 {
|
||||
output.push(match tag_name {
|
||||
Some(_name) => Event::new(Some(tag_id), event_description),
|
||||
|
||||
None => Event::new(None, event_description),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,3 @@
|
|||
#[derive(Default, Clone)]
|
||||
pub struct DayInfo {
|
||||
pub events: Vec<Event>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct Event {
|
||||
pub tag: Option<u64>,
|
||||
|
|
@ -14,9 +9,3 @@ impl Event {
|
|||
Self { tag, description }
|
||||
}
|
||||
}
|
||||
|
||||
impl DayInfo {
|
||||
pub fn new(events: Vec<Event>) -> Self {
|
||||
Self { events }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
132
src/days.rs
132
src/days.rs
|
|
@ -1,8 +1,7 @@
|
|||
use crate::{
|
||||
app::FocusedComponent, database::DB, day_info::DayInfo, events::AppEvent, focused::Focused,
|
||||
};
|
||||
use chrono::{Datelike, Duration, NaiveDate};
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use crate::{app::FocusedComponent, database::DB, events::AppEvent, focused::Focused};
|
||||
use chrono::{Datelike, Duration, NaiveDate, Weekday};
|
||||
use ratatui::crossterm::event::{KeyCode, KeyEvent};
|
||||
use ratatui::text::Span;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Layout, Margin, Rect},
|
||||
|
|
@ -23,8 +22,6 @@ pub struct Days {
|
|||
selected_day: NaiveDate,
|
||||
pivot: Option<NaiveDate>,
|
||||
|
||||
date_of_first_day: i32,
|
||||
|
||||
current_month: i32,
|
||||
current_year: i32,
|
||||
}
|
||||
|
|
@ -42,7 +39,7 @@ enum CursorMovement {
|
|||
}
|
||||
|
||||
impl Days {
|
||||
pub fn ready_to_render(&mut self, area: Rect) {
|
||||
pub fn ready_to_render(&mut self, area: Rect, db: &mut DB) {
|
||||
let view_rect = area.inner(Margin::new(1, 1));
|
||||
let number_of_days_shown = view_rect.height;
|
||||
|
||||
|
|
@ -89,6 +86,28 @@ impl Days {
|
|||
}
|
||||
self.days.push_front(next_day);
|
||||
}
|
||||
|
||||
for i in self.days.iter_mut() {
|
||||
if i.info.is_none() {
|
||||
i.info = Some(
|
||||
db.get_day(i.day)
|
||||
.expect("Database error")
|
||||
.unwrap_or_default()
|
||||
.len(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reload_day_counts(&mut self, db: &mut DB) {
|
||||
for i in self.days.iter_mut() {
|
||||
i.info = Some(
|
||||
db.get_day(i.day)
|
||||
.expect("Database error")
|
||||
.unwrap_or_default()
|
||||
.len(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reload(&mut self, month: i32, year: i32, day: i32) {
|
||||
|
|
@ -100,7 +119,6 @@ impl Days {
|
|||
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>> {
|
||||
|
|
@ -186,7 +204,17 @@ impl Days {
|
|||
self.state = DaysState::Main;
|
||||
None
|
||||
}
|
||||
KeyCode::Enter => None,
|
||||
KeyCode::Enter => Some(vec![AppEvent::DaySelected(self.selected_day)]),
|
||||
KeyCode::Char('a') => {
|
||||
let mut start = self.pivot.unwrap_or(self.selected_day);
|
||||
let mut end = self.selected_day;
|
||||
|
||||
if start > end {
|
||||
std::mem::swap(&mut start, &mut end);
|
||||
}
|
||||
|
||||
Some(vec![AppEvent::AddEvent(start, end)])
|
||||
}
|
||||
KeyCode::Down => self.handle_focused_arrows(CursorMovement::Down),
|
||||
KeyCode::Up => self.handle_focused_arrows(CursorMovement::Up),
|
||||
KeyCode::Char(' ') => {
|
||||
|
|
@ -241,7 +269,7 @@ impl Widget for &Days {
|
|||
.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)
|
||||
day.get_line(self.selected_day, self.pivot, inner_area.width as usize)
|
||||
.render(*area, buf);
|
||||
}
|
||||
}
|
||||
|
|
@ -253,43 +281,71 @@ impl Widget for &Days {
|
|||
#[derive(Clone)]
|
||||
pub struct Day {
|
||||
pub day: NaiveDate,
|
||||
pub info: DayInfo,
|
||||
pub info: Option<usize>,
|
||||
}
|
||||
|
||||
impl Day {
|
||||
pub fn new(day: NaiveDate) -> Self {
|
||||
Self {
|
||||
day,
|
||||
info: DayInfo::default(),
|
||||
Self { day, info: None }
|
||||
}
|
||||
}
|
||||
|
||||
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!(
|
||||
fn get_line(&self, current_day: NaiveDate, pivot: Option<NaiveDate>, width: usize) -> Line<'_> {
|
||||
let day_of_week = match self.day.weekday() {
|
||||
Weekday::Mon => "Mon",
|
||||
Weekday::Tue => "Tue",
|
||||
Weekday::Wed => "Wed",
|
||||
Weekday::Thu => "Thu",
|
||||
Weekday::Fri => "Fri",
|
||||
Weekday::Sat => "Sat",
|
||||
Weekday::Sun => "Sun",
|
||||
};
|
||||
let is_weekend = matches!(self.day.weekday(), Weekday::Sat | Weekday::Sun);
|
||||
let day_text_color = if is_weekend {
|
||||
Color::DarkGray
|
||||
} else {
|
||||
Color::White
|
||||
};
|
||||
let date_text = format!(
|
||||
"{}, {}, {}",
|
||||
self.day.day(),
|
||||
self.day.month(),
|
||||
self.day.year()
|
||||
),
|
||||
Style::new().bg(if Days::is_date_between(current_day, pivot, self.day) {
|
||||
);
|
||||
let bg_color = 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
|
||||
}),
|
||||
)
|
||||
};
|
||||
|
||||
let l = date_text.len();
|
||||
|
||||
let mut spans = vec![
|
||||
Span::styled(day_of_week, Style::new().fg(day_text_color)),
|
||||
Span::styled(" ", Style::new()),
|
||||
Span::styled(date_text, Style::new().fg(day_text_color)),
|
||||
];
|
||||
|
||||
match self.info {
|
||||
None | Some(0) => {
|
||||
let current_length = day_of_week.len() + 1 + l;
|
||||
if current_length < width {
|
||||
spans.push(Span::styled(
|
||||
" ".repeat(width - current_length),
|
||||
Style::new(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Some(info_value) => {
|
||||
let info_text = format!("({})", info_value);
|
||||
let current_length = day_of_week.len() + 1 + l;
|
||||
let available_space = width.saturating_sub(current_length + info_text.len());
|
||||
|
||||
if available_space > 0 {
|
||||
spans.push(Span::styled(" ".repeat(available_space), Style::new()));
|
||||
}
|
||||
|
||||
spans.push(Span::styled(info_text, Style::new()));
|
||||
}
|
||||
}
|
||||
|
||||
Line::from(spans).style(Style::new().bg(bg_color))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use crate::app::FocusedComponent;
|
||||
use crate::day_info::DayInfo;
|
||||
use chrono::NaiveDate;
|
||||
|
||||
pub enum AppEvent {
|
||||
SwitchFocus(FocusedComponent),
|
||||
DaySelected(DayInfo),
|
||||
DaySelected(NaiveDate),
|
||||
|
||||
YearSet(i32),
|
||||
MonthSet(i32),
|
||||
|
|
@ -11,6 +11,10 @@ pub enum AppEvent {
|
|||
YearScrolled(Direction),
|
||||
MonthScrolled(Direction),
|
||||
|
||||
Reload,
|
||||
|
||||
AddEvent(NaiveDate, NaiveDate),
|
||||
|
||||
Exit,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crossterm::event::KeyEvent;
|
||||
use ratatui::crossterm::event::KeyEvent;
|
||||
|
||||
use crate::database::DB;
|
||||
use crate::events::AppEvent;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ mod day_info;
|
|||
mod days;
|
||||
mod events;
|
||||
mod focused;
|
||||
mod global_shortcuts;
|
||||
mod joiner;
|
||||
mod month;
|
||||
mod popup;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use ratatui::crossterm::event::{KeyCode, KeyEvent};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
|
|
|
|||
68
src/popup.rs
68
src/popup.rs
|
|
@ -1,68 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
225
src/popup/day_details.rs
Normal file
225
src/popup/day_details.rs
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
use crate::day_info::Event;
|
||||
use crate::{app::FocusedComponent, database::DB, events::AppEvent, focused::Focused};
|
||||
use chrono::NaiveDate;
|
||||
use ratatui::crossterm::event::KeyCode;
|
||||
use ratatui::crossterm::event::KeyEvent;
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Margin};
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::symbols::border;
|
||||
use ratatui::widgets::{Block, Clear, Padding, Paragraph, Wrap};
|
||||
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DayDetails {
|
||||
pub events: Vec<Event>,
|
||||
tag_names: Vec<String>,
|
||||
day: NaiveDate,
|
||||
|
||||
state: State,
|
||||
selected_event: usize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum State {
|
||||
#[default]
|
||||
EventSelect,
|
||||
DeletionConfirm(bool),
|
||||
}
|
||||
|
||||
impl DayDetails {
|
||||
pub fn ready_to_render(&mut self, _area: Rect) {}
|
||||
|
||||
pub fn show_day(day: NaiveDate, events: Vec<Event>, tag_names: Vec<String>) -> Self {
|
||||
Self {
|
||||
events,
|
||||
day,
|
||||
tag_names,
|
||||
state: State::default(),
|
||||
selected_event: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Focused for DayDetails {
|
||||
fn take_focus(&mut self) {}
|
||||
|
||||
fn lose_focus(&mut self) {}
|
||||
|
||||
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)]),
|
||||
|
||||
t => {
|
||||
if self.events.is_empty() {
|
||||
return None;
|
||||
}
|
||||
match &mut self.state {
|
||||
State::EventSelect => match t {
|
||||
KeyCode::Right => {
|
||||
self.selected_event = (self.selected_event + 1) % self.events.len();
|
||||
None
|
||||
}
|
||||
|
||||
KeyCode::Left => {
|
||||
if self.selected_event == 0 {
|
||||
self.selected_event = self.events.len() - 1;
|
||||
} else {
|
||||
self.selected_event -= 1;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
KeyCode::Enter => {
|
||||
self.state = State::DeletionConfirm(false);
|
||||
None
|
||||
}
|
||||
|
||||
_ => None,
|
||||
},
|
||||
|
||||
State::DeletionConfirm(confirm) => match t {
|
||||
KeyCode::Right => {
|
||||
*confirm = false;
|
||||
None
|
||||
}
|
||||
|
||||
KeyCode::Left => {
|
||||
*confirm = true;
|
||||
None
|
||||
}
|
||||
|
||||
KeyCode::Enter => {
|
||||
if *confirm {
|
||||
db.remove_event(self.day, self.selected_event)
|
||||
.expect("Database error");
|
||||
Some(vec![
|
||||
AppEvent::SwitchFocus(FocusedComponent::Days),
|
||||
AppEvent::Reload,
|
||||
])
|
||||
} else {
|
||||
self.state = State::EventSelect;
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
_ => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DayDetails {
|
||||
pub fn render(&self, area: Rect, buf: &mut Buffer, _focused: bool) {
|
||||
Clear.render(area, buf);
|
||||
|
||||
if self.events.is_empty() {
|
||||
let outline = Block::bordered()
|
||||
.border_set(border::ROUNDED)
|
||||
.border_style(Style::new().fg(Color::Blue));
|
||||
|
||||
let no_events_text = Paragraph::new("No events for this day")
|
||||
.style(Style::default().fg(Color::Red))
|
||||
.alignment(Alignment::Center)
|
||||
.block(outline);
|
||||
|
||||
no_events_text.render(area, buf);
|
||||
return;
|
||||
}
|
||||
match self.state {
|
||||
State::EventSelect => {
|
||||
let outline = Block::bordered()
|
||||
.border_set(border::ROUNDED)
|
||||
.border_style(Style::new().fg(Color::Blue));
|
||||
let layout = Layout::vertical([Constraint::Length(3), Constraint::Fill(1)])
|
||||
.split(outline.inner(area));
|
||||
outline.render(area, buf);
|
||||
let event_select = Block::bordered()
|
||||
.border_set(border::ROUNDED)
|
||||
.border_style(Style::default().fg(Color::Blue));
|
||||
let event_description = Block::bordered()
|
||||
.border_set(border::ROUNDED)
|
||||
.border_style(Style::default().fg(Color::Blue));
|
||||
|
||||
let select_area = event_select.inner(layout[0]);
|
||||
let description_area = event_description.inner(layout[1]);
|
||||
|
||||
event_select.render(layout[0], buf);
|
||||
event_description.render(layout[1], buf);
|
||||
|
||||
let event_select_span = Paragraph::new(format!(
|
||||
"({}/{}) {}",
|
||||
self.selected_event + 1,
|
||||
self.events.len(),
|
||||
self.tag_names[self.selected_event].clone()
|
||||
));
|
||||
event_select_span.render(select_area, buf);
|
||||
|
||||
let description_span =
|
||||
Paragraph::new(self.events[self.selected_event].description.clone())
|
||||
.wrap(Wrap { trim: true });
|
||||
|
||||
description_span.render(description_area, buf);
|
||||
}
|
||||
|
||||
State::DeletionConfirm(confirm) => {
|
||||
let block = Block::bordered()
|
||||
.title(" Manage Tags ")
|
||||
.title_alignment(Alignment::Center)
|
||||
.border_set(border::ROUNDED)
|
||||
.border_style(Style::default().fg(Color::Blue))
|
||||
.padding(Padding::uniform(1))
|
||||
.bg(Color::Reset);
|
||||
|
||||
let inner_area = block.inner(area);
|
||||
block.render(area, buf);
|
||||
|
||||
let layout =
|
||||
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(inner_area);
|
||||
|
||||
let new_button = Paragraph::new("Delete")
|
||||
.style(Style::default().fg(if confirm { Color::Green } else { Color::White }))
|
||||
.alignment(Alignment::Center)
|
||||
.block(Block::bordered().border_set(border::ROUNDED).border_style(
|
||||
if confirm {
|
||||
Style::default().fg(Color::Green)
|
||||
} else {
|
||||
Style::default().fg(Color::White)
|
||||
},
|
||||
));
|
||||
|
||||
let delete_button = Paragraph::new("Cancel deletion")
|
||||
.style(Style::default().fg(if !confirm { Color::Red } else { Color::White }))
|
||||
.alignment(Alignment::Center)
|
||||
.block(Block::bordered().border_set(border::ROUNDED).border_style(
|
||||
if !confirm {
|
||||
Style::default().fg(Color::Red)
|
||||
} else {
|
||||
Style::default().fg(Color::White)
|
||||
},
|
||||
));
|
||||
|
||||
new_button.render(layout[0].inner(Margin::new(1, 2)), buf);
|
||||
delete_button.render(layout[1].inner(Margin::new(1, 2)), buf);
|
||||
let instructions = Paragraph::new(format!(
|
||||
"Do you really want to delete tag: {}",
|
||||
self.tag_names
|
||||
.get(self.selected_event)
|
||||
.expect("User selected invalid tag")
|
||||
));
|
||||
|
||||
let instructions_area = Rect {
|
||||
x: inner_area.x,
|
||||
y: inner_area.y,
|
||||
width: inner_area.width,
|
||||
height: 1,
|
||||
};
|
||||
instructions.render(instructions_area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
301
src/popup/event_adding.rs
Normal file
301
src/popup/event_adding.rs
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
use crate::day_info::Event;
|
||||
use crate::{app::FocusedComponent, database::DB, events::AppEvent, focused::Focused};
|
||||
use chrono::NaiveDate;
|
||||
use ratatui::crossterm::event::{KeyCode, KeyEvent};
|
||||
use ratatui::layout::{Alignment, Constraint, Layout};
|
||||
use ratatui::prelude::Color;
|
||||
use ratatui::style::{Modifier, Style, Stylize};
|
||||
use ratatui::symbols::border;
|
||||
use ratatui::widgets::{Block, Clear, List, ListItem, ListState, Padding, Paragraph};
|
||||
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
|
||||
use std::boxed::Box;
|
||||
use tui_textarea::TextArea;
|
||||
|
||||
pub enum EventAddingState<'a> {
|
||||
SelectingTag(ListState),
|
||||
WritingDescription(u64, Box<TextArea<'a>>), // selected_tag_id, text_area
|
||||
}
|
||||
|
||||
impl Default for EventAddingState<'_> {
|
||||
fn default() -> Self {
|
||||
let mut list_state = ListState::default();
|
||||
list_state.select(Some(0));
|
||||
EventAddingState::SelectingTag(list_state)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EventAdding<'a> {
|
||||
state: EventAddingState<'a>,
|
||||
start: NaiveDate,
|
||||
end: NaiveDate,
|
||||
tags: Vec<(u64, String)>,
|
||||
}
|
||||
|
||||
impl Focused for EventAdding<'_> {
|
||||
fn take_focus(&mut self) {}
|
||||
|
||||
fn lose_focus(&mut self) {}
|
||||
|
||||
fn handle_input(&mut self, key_event: KeyEvent, db: &mut DB) -> Option<Vec<AppEvent>> {
|
||||
match key_event.code {
|
||||
KeyCode::Esc => match &self.state {
|
||||
EventAddingState::SelectingTag(_) => {
|
||||
Some(vec![AppEvent::SwitchFocus(FocusedComponent::Days)])
|
||||
}
|
||||
EventAddingState::WritingDescription(_, _) => {
|
||||
// Go back to tag selection
|
||||
let mut list_state = ListState::default();
|
||||
list_state.select(Some(0));
|
||||
self.state = EventAddingState::SelectingTag(list_state);
|
||||
None
|
||||
}
|
||||
},
|
||||
_ => match &self.state {
|
||||
EventAddingState::SelectingTag(_) => self.handle_tag_selection_input(key_event, db),
|
||||
EventAddingState::WritingDescription(_, _) => {
|
||||
self.handle_description_input(key_event, db)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventAdding<'_> {
|
||||
pub fn ready_to_render(&mut self, _area: Rect) {}
|
||||
|
||||
pub fn add_event(start: NaiveDate, end: NaiveDate, tags: Vec<(u64, String)>) -> Self {
|
||||
Self {
|
||||
state: EventAddingState::default(),
|
||||
start,
|
||||
end,
|
||||
tags,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&self, area: Rect, buf: &mut Buffer, _focused: bool) {
|
||||
match &self.state {
|
||||
EventAddingState::SelectingTag(list_state) => {
|
||||
self.render_tag_selection(area, buf, list_state)
|
||||
}
|
||||
EventAddingState::WritingDescription(tag_id, text_area) => {
|
||||
self.render_description_input(area, buf, *tag_id, text_area)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_tag_selection(&self, area: Rect, buf: &mut Buffer, list_state: &ListState) {
|
||||
Clear.render(area, buf);
|
||||
|
||||
let block = Block::bordered()
|
||||
.title(" Create Event - Select Tag ")
|
||||
.title_alignment(Alignment::Center)
|
||||
.border_set(border::ROUNDED)
|
||||
.border_style(Style::default().fg(Color::Blue))
|
||||
.padding(Padding::uniform(1))
|
||||
.bg(Color::Reset);
|
||||
|
||||
let inner_area = block.inner(area);
|
||||
block.render(area, buf);
|
||||
|
||||
if self.tags.is_empty() {
|
||||
let no_tags_msg = Paragraph::new("No tags available. Create tags first!")
|
||||
.style(Style::default().fg(Color::Red))
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
let msg_area = Rect {
|
||||
x: inner_area.x,
|
||||
y: inner_area.y + inner_area.height / 2,
|
||||
width: inner_area.width,
|
||||
height: 1,
|
||||
};
|
||||
no_tags_msg.render(msg_area, buf);
|
||||
} else {
|
||||
let tag_items: Vec<ListItem> = self
|
||||
.tags
|
||||
.iter()
|
||||
.map(|(_, name)| ListItem::new(name.as_str()))
|
||||
.collect();
|
||||
|
||||
let tags_list = List::new(tag_items)
|
||||
.block(
|
||||
Block::bordered()
|
||||
.title("Available Tags")
|
||||
.border_set(border::ROUNDED)
|
||||
.border_style(Style::default().fg(Color::White)),
|
||||
)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(Color::Blue)
|
||||
.fg(Color::White)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_symbol("→ ");
|
||||
|
||||
let list_area = Rect {
|
||||
x: inner_area.x,
|
||||
y: inner_area.y,
|
||||
width: inner_area.width,
|
||||
height: inner_area.height.saturating_sub(2),
|
||||
};
|
||||
|
||||
ratatui::widgets::StatefulWidget::render(
|
||||
tags_list,
|
||||
list_area,
|
||||
buf,
|
||||
&mut list_state.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
let instructions = if self.tags.is_empty() {
|
||||
Paragraph::new("Esc to cancel")
|
||||
} else {
|
||||
Paragraph::new("↑↓ to navigate, Enter to select tag, Esc to cancel")
|
||||
};
|
||||
|
||||
let instructions_area = Rect {
|
||||
x: inner_area.x,
|
||||
y: inner_area.y + inner_area.height.saturating_sub(1),
|
||||
width: inner_area.width,
|
||||
height: 1,
|
||||
};
|
||||
instructions.render(instructions_area, buf);
|
||||
}
|
||||
|
||||
fn render_description_input(
|
||||
&self,
|
||||
area: Rect,
|
||||
buf: &mut Buffer,
|
||||
tag_id: u64,
|
||||
text_area: &TextArea,
|
||||
) {
|
||||
Clear.render(area, buf);
|
||||
|
||||
let tag_name = self
|
||||
.tags
|
||||
.iter()
|
||||
.find(|(id, _)| *id == tag_id)
|
||||
.map(|(_, name)| name.as_str())
|
||||
.unwrap_or("Unknown Tag");
|
||||
|
||||
let block = Block::bordered()
|
||||
.title(format!(" Create Event - Tag: {} ", tag_name))
|
||||
.title_alignment(Alignment::Center)
|
||||
.border_set(border::ROUNDED)
|
||||
.border_style(Style::default().fg(Color::Blue))
|
||||
.padding(Padding::uniform(1))
|
||||
.bg(Color::Reset);
|
||||
|
||||
let inner_area = block.inner(area);
|
||||
block.render(area, buf);
|
||||
|
||||
let layout = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(3),
|
||||
Constraint::Length(2),
|
||||
])
|
||||
.split(inner_area);
|
||||
|
||||
let label = Paragraph::new("Event Description:").style(Style::default().fg(Color::White));
|
||||
label.render(layout[0], buf);
|
||||
|
||||
text_area.render(layout[1], buf);
|
||||
|
||||
let instructions =
|
||||
Paragraph::new("Enter description and press Ctrl+X to save, Esc to go back")
|
||||
.style(Style::default().fg(Color::Gray));
|
||||
instructions.render(layout[2], buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl EventAdding<'_> {
|
||||
fn handle_tag_selection_input(
|
||||
&mut self,
|
||||
key_event: KeyEvent,
|
||||
_db: &mut DB,
|
||||
) -> Option<Vec<AppEvent>> {
|
||||
if let EventAddingState::SelectingTag(list_state) = &mut self.state {
|
||||
match key_event.code {
|
||||
KeyCode::Up => {
|
||||
if !self.tags.is_empty() {
|
||||
let selected = list_state.selected().unwrap_or(0);
|
||||
let new_index = if selected == 0 {
|
||||
self.tags.len() - 1
|
||||
} else {
|
||||
selected - 1
|
||||
};
|
||||
list_state.select(Some(new_index));
|
||||
}
|
||||
None
|
||||
}
|
||||
KeyCode::Down => {
|
||||
if !self.tags.is_empty() {
|
||||
let selected = list_state.selected().unwrap_or(0);
|
||||
let new_index = (selected + 1) % self.tags.len();
|
||||
list_state.select(Some(new_index));
|
||||
}
|
||||
None
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if !self.tags.is_empty()
|
||||
&& let Some(selected) = list_state.selected()
|
||||
&& let Some((tag_id, _)) = self.tags.get(selected)
|
||||
{
|
||||
let mut text_area = TextArea::default();
|
||||
text_area.set_block(
|
||||
Block::bordered()
|
||||
.border_set(border::ROUNDED)
|
||||
.border_style(Style::default().fg(Color::Green)),
|
||||
);
|
||||
self.state =
|
||||
EventAddingState::WritingDescription(*tag_id, Box::new(text_area));
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_description_input(
|
||||
&mut self,
|
||||
key_event: KeyEvent,
|
||||
db: &mut DB,
|
||||
) -> Option<Vec<AppEvent>> {
|
||||
if let EventAddingState::WritingDescription(tag_id, text_area) = &mut self.state {
|
||||
match key_event.code {
|
||||
KeyCode::Char('x')
|
||||
if key_event
|
||||
.modifiers
|
||||
.contains(ratatui::crossterm::event::KeyModifiers::CONTROL) =>
|
||||
{
|
||||
let description = text_area.lines().join("\n").trim().to_string();
|
||||
if !description.is_empty() {
|
||||
let mut cur_day = self.start;
|
||||
|
||||
while cur_day <= self.end {
|
||||
db.add_event(cur_day, Event::new(Some(*tag_id), description.clone()))
|
||||
.expect("Database error");
|
||||
cur_day = cur_day.checked_add_days(chrono::Days::new(1)).unwrap();
|
||||
}
|
||||
|
||||
Some(vec![
|
||||
AppEvent::SwitchFocus(FocusedComponent::Days),
|
||||
AppEvent::Reload,
|
||||
])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
text_area.input(key_event);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
91
src/popup/mod.rs
Normal file
91
src/popup/mod.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
mod day_details;
|
||||
mod event_adding;
|
||||
mod tag_management;
|
||||
|
||||
use crate::day_info::Event;
|
||||
use crate::{database::DB, events::AppEvent, focused::Focused};
|
||||
use chrono::NaiveDate;
|
||||
use ratatui::crossterm::event::KeyEvent;
|
||||
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
|
||||
|
||||
pub use day_details::DayDetails;
|
||||
pub use event_adding::EventAdding;
|
||||
pub use tag_management::TagManagement;
|
||||
|
||||
pub enum PopupType<'a> {
|
||||
TagManagement(TagManagement<'a>),
|
||||
DayDetails(DayDetails),
|
||||
EventAdding(EventAdding<'a>),
|
||||
}
|
||||
|
||||
impl Default for PopupType<'_> {
|
||||
fn default() -> Self {
|
||||
PopupType::TagManagement(TagManagement::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Popup<'a> {
|
||||
pub self_state: PopupType<'a>,
|
||||
pub focused: bool,
|
||||
}
|
||||
|
||||
impl Popup<'_> {
|
||||
pub fn ready_to_render(&mut self, area: Rect) {
|
||||
match &mut self.self_state {
|
||||
PopupType::TagManagement(tm) => tm.ready_to_render(area),
|
||||
PopupType::DayDetails(dd) => dd.ready_to_render(area),
|
||||
PopupType::EventAdding(ea) => ea.ready_to_render(area),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_event(&mut self, start: NaiveDate, end: NaiveDate, tags: Vec<(u64, String)>) {
|
||||
self.self_state = PopupType::EventAdding(EventAdding::add_event(start, end, tags));
|
||||
}
|
||||
|
||||
pub fn show_day(&mut self, day: NaiveDate, events: Vec<Event>, tag_names: Vec<String>) {
|
||||
self.self_state = PopupType::DayDetails(DayDetails::show_day(day, events, tag_names));
|
||||
}
|
||||
}
|
||||
|
||||
impl Focused for Popup<'_> {
|
||||
fn take_focus(&mut self) {
|
||||
self.focused = true;
|
||||
match &mut self.self_state {
|
||||
PopupType::TagManagement(tm) => tm.take_focus(),
|
||||
PopupType::DayDetails(dd) => dd.take_focus(),
|
||||
PopupType::EventAdding(ea) => ea.take_focus(),
|
||||
}
|
||||
}
|
||||
|
||||
fn lose_focus(&mut self) {
|
||||
self.focused = false;
|
||||
match &mut self.self_state {
|
||||
PopupType::TagManagement(tm) => tm.lose_focus(),
|
||||
PopupType::DayDetails(dd) => dd.lose_focus(),
|
||||
PopupType::EventAdding(ea) => ea.lose_focus(),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_input(&mut self, key_event: KeyEvent, db: &mut DB) -> Option<Vec<AppEvent>> {
|
||||
match &mut self.self_state {
|
||||
PopupType::TagManagement(tm) => tm.handle_input(key_event, db),
|
||||
PopupType::DayDetails(dd) => dd.handle_input(key_event, db),
|
||||
PopupType::EventAdding(ea) => ea.handle_input(key_event, db),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &Popup<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if !self.focused {
|
||||
return;
|
||||
}
|
||||
|
||||
match &self.self_state {
|
||||
PopupType::TagManagement(tm) => tm.render(area, buf, self.focused),
|
||||
PopupType::DayDetails(dd) => dd.render(area, buf, self.focused),
|
||||
PopupType::EventAdding(ea) => ea.render(area, buf, self.focused),
|
||||
}
|
||||
}
|
||||
}
|
||||
340
src/popup/tag_management.rs
Normal file
340
src/popup/tag_management.rs
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
use crate::{app::FocusedComponent, database::DB, events::AppEvent, focused::Focused};
|
||||
use ratatui::crossterm::event::{KeyCode, KeyEvent};
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Margin};
|
||||
use ratatui::prelude::Color;
|
||||
use ratatui::style::{Modifier, Style, Stylize};
|
||||
use ratatui::symbols::border;
|
||||
use ratatui::widgets::{Block, Clear, Padding, Paragraph};
|
||||
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
|
||||
use std::boxed::Box;
|
||||
use tui_textarea::TextArea;
|
||||
|
||||
pub enum TagManagementState<'a> {
|
||||
Undecided(bool),
|
||||
New(Box<TextArea<'a>>),
|
||||
Delete(usize, bool, bool), //index_of_deleted_tag, needs_confirmation, choosing_yes
|
||||
}
|
||||
|
||||
impl Default for TagManagementState<'_> {
|
||||
fn default() -> Self {
|
||||
TagManagementState::Undecided(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TagManagement<'a> {
|
||||
state: TagManagementState<'a>,
|
||||
tags: Vec<(u64, String)>,
|
||||
}
|
||||
|
||||
impl Focused for TagManagement<'_> {
|
||||
fn take_focus(&mut self) {}
|
||||
|
||||
fn lose_focus(&mut self) {}
|
||||
|
||||
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)]),
|
||||
_ => match &self.state {
|
||||
TagManagementState::Undecided(_) => self.handle_choice_input(key_event, db),
|
||||
TagManagementState::New(_) => self.handle_new_tag_input(key_event, db),
|
||||
TagManagementState::Delete(_, _, _) => self.handle_delete_tag_input(key_event, db),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TagManagement<'_> {
|
||||
pub fn ready_to_render(&mut self, _area: Rect) {}
|
||||
|
||||
pub fn render(&self, area: Rect, buf: &mut Buffer, _focused: bool) {
|
||||
match &self.state {
|
||||
TagManagementState::Undecided(choice) => self.render_choice_dialog(area, buf, *choice),
|
||||
TagManagementState::New(text_area) => self.render_new_tag_dialog(area, buf, text_area),
|
||||
TagManagementState::Delete(index_of_selected_tag, confirm, choosing_yes) => {
|
||||
if !*confirm {
|
||||
self.render_delete_tag_dialog(area, buf, *index_of_selected_tag)
|
||||
} else {
|
||||
self.render_confirm(area, buf, *index_of_selected_tag, *choosing_yes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_confirm(&self, area: Rect, buf: &mut Buffer, index: usize, choosing_yes: bool) {
|
||||
Clear.render(area, buf);
|
||||
|
||||
let block = Block::bordered()
|
||||
.title(" Manage Tags ")
|
||||
.title_alignment(Alignment::Center)
|
||||
.border_set(border::ROUNDED)
|
||||
.border_style(Style::default().fg(Color::Blue))
|
||||
.padding(Padding::uniform(1))
|
||||
.bg(Color::Reset);
|
||||
|
||||
let inner_area = block.inner(area);
|
||||
block.render(area, buf);
|
||||
|
||||
let layout = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(inner_area);
|
||||
|
||||
let new_button =
|
||||
Paragraph::new("Delete")
|
||||
.style(Style::default().fg(if choosing_yes {
|
||||
Color::Green
|
||||
} else {
|
||||
Color::White
|
||||
}))
|
||||
.alignment(Alignment::Center)
|
||||
.block(Block::bordered().border_set(border::ROUNDED).border_style(
|
||||
if choosing_yes {
|
||||
Style::default().fg(Color::Green)
|
||||
} else {
|
||||
Style::default().fg(Color::White)
|
||||
},
|
||||
));
|
||||
|
||||
let delete_button =
|
||||
Paragraph::new("Cancel deletion")
|
||||
.style(Style::default().fg(if !choosing_yes {
|
||||
Color::Red
|
||||
} else {
|
||||
Color::White
|
||||
}))
|
||||
.alignment(Alignment::Center)
|
||||
.block(Block::bordered().border_set(border::ROUNDED).border_style(
|
||||
if !choosing_yes {
|
||||
Style::default().fg(Color::Red)
|
||||
} else {
|
||||
Style::default().fg(Color::White)
|
||||
},
|
||||
));
|
||||
|
||||
new_button.render(layout[0].inner(Margin::new(1, 2)), buf);
|
||||
delete_button.render(layout[1].inner(Margin::new(1, 2)), buf);
|
||||
let instructions = Paragraph::new(format!(
|
||||
"Do you really want to delete tag: {}",
|
||||
self.tags.get(index).expect("User selected invalid tag").1
|
||||
));
|
||||
|
||||
let instructions_area = Rect {
|
||||
x: inner_area.x,
|
||||
y: inner_area.y,
|
||||
width: inner_area.width,
|
||||
height: 1,
|
||||
};
|
||||
instructions.render(instructions_area, buf);
|
||||
}
|
||||
|
||||
fn render_choice_dialog(&self, area: Rect, buf: &mut Buffer, choice: bool) {
|
||||
Clear.render(area, buf);
|
||||
|
||||
let block = Block::bordered()
|
||||
.title(" Manage Tags ")
|
||||
.title_alignment(Alignment::Center)
|
||||
.border_set(border::ROUNDED)
|
||||
.border_style(Style::default().fg(Color::Blue))
|
||||
.padding(Padding::uniform(1))
|
||||
.bg(Color::Reset);
|
||||
|
||||
let inner_area = block.inner(area);
|
||||
block.render(area, buf);
|
||||
|
||||
let layout = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(inner_area);
|
||||
|
||||
let new_button = Paragraph::new("New")
|
||||
.style(Style::default().fg(if choice { Color::Green } else { Color::White }))
|
||||
.alignment(Alignment::Center)
|
||||
.block(
|
||||
Block::bordered()
|
||||
.border_set(border::ROUNDED)
|
||||
.border_style(if choice {
|
||||
Style::default().fg(Color::Green)
|
||||
} else {
|
||||
Style::default().fg(Color::White)
|
||||
}),
|
||||
);
|
||||
|
||||
let delete_button = Paragraph::new("Delete")
|
||||
.style(Style::default().fg(if !choice { Color::Red } else { Color::White }))
|
||||
.alignment(Alignment::Center)
|
||||
.block(
|
||||
Block::bordered()
|
||||
.border_set(border::ROUNDED)
|
||||
.border_style(if !choice {
|
||||
Style::default().fg(Color::Red)
|
||||
} else {
|
||||
Style::default().fg(Color::White)
|
||||
}),
|
||||
);
|
||||
|
||||
new_button.render(layout[0].inner(Margin::new(1, 2)), buf);
|
||||
delete_button.render(layout[1].inner(Margin::new(1, 2)), buf);
|
||||
}
|
||||
|
||||
fn render_new_tag_dialog(&self, area: Rect, buf: &mut Buffer, text_area: &TextArea) {
|
||||
Clear.render(area, buf);
|
||||
|
||||
let block = Block::bordered()
|
||||
.title("New Tag")
|
||||
.border_set(border::ROUNDED)
|
||||
.border_style(Style::new().fg(Color::Blue));
|
||||
|
||||
let inner_area = block.inner(area);
|
||||
block.render(area, buf);
|
||||
|
||||
text_area.render(inner_area, buf);
|
||||
|
||||
let instructions =
|
||||
Paragraph::new("Enter tag name and press Enter to confirm, Esc to cancel");
|
||||
let instructions_area = Rect {
|
||||
x: inner_area.x,
|
||||
y: inner_area.y + inner_area.height.saturating_sub(1),
|
||||
width: inner_area.width,
|
||||
height: 1,
|
||||
};
|
||||
instructions.render(instructions_area, buf);
|
||||
}
|
||||
|
||||
fn render_delete_tag_dialog(&self, area: Rect, buf: &mut Buffer, index: usize) {
|
||||
Clear.render(area, buf);
|
||||
let block = Block::bordered()
|
||||
.title("Delete Tag")
|
||||
.border_set(border::ROUNDED)
|
||||
.border_style(Style::new().fg(Color::Blue));
|
||||
let inner_area = block.inner(area);
|
||||
block.render(area, buf);
|
||||
|
||||
if !self.tags.is_empty() && index < self.tags.len() {
|
||||
let current_tag = &self.tags[index].1;
|
||||
let tag_display = format!("{} ({}/{})", current_tag, index + 1, self.tags.len());
|
||||
|
||||
let tag_paragraph = Paragraph::new(tag_display)
|
||||
.style(Style::default().add_modifier(Modifier::REVERSED))
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
let tag_area = Rect {
|
||||
x: inner_area.x,
|
||||
y: inner_area.y + inner_area.height / 2,
|
||||
width: inner_area.width,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
tag_paragraph.render(tag_area, buf);
|
||||
}
|
||||
|
||||
let instructions =
|
||||
Paragraph::new("Navigate with Left/Right, Enter to confirm, Esc to cancel")
|
||||
.alignment(Alignment::Center);
|
||||
let instructions_area = Rect {
|
||||
x: inner_area.x,
|
||||
y: inner_area.y + inner_area.height.saturating_sub(1),
|
||||
width: inner_area.width,
|
||||
height: 1,
|
||||
};
|
||||
instructions.render(instructions_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl TagManagement<'_> {
|
||||
fn handle_choice_input(&mut self, key_event: KeyEvent, db: &mut DB) -> Option<Vec<AppEvent>> {
|
||||
if let TagManagementState::Undecided(choice) = &mut self.state {
|
||||
match key_event.code {
|
||||
KeyCode::Right => {
|
||||
*choice = false;
|
||||
None
|
||||
}
|
||||
KeyCode::Left => {
|
||||
*choice = true;
|
||||
None
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if *choice {
|
||||
self.state = TagManagementState::New(Box::new(TextArea::default()));
|
||||
} else {
|
||||
self.tags = db.get_tags().expect("Database error");
|
||||
self.state = TagManagementState::Delete(0, false, false);
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_new_tag_input(&mut self, key_event: KeyEvent, db: &mut DB) -> Option<Vec<AppEvent>> {
|
||||
if let TagManagementState::New(text_area) = &mut self.state {
|
||||
match key_event.code {
|
||||
KeyCode::Enter => {
|
||||
let tag_name = text_area.lines()[0].trim();
|
||||
if !tag_name.is_empty() {
|
||||
db.create_tag(tag_name.to_string()).expect("Database error");
|
||||
self.state = TagManagementState::Undecided(false);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
_ => {
|
||||
text_area.input(key_event);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_delete_tag_input(
|
||||
&mut self,
|
||||
key_event: KeyEvent,
|
||||
db: &mut DB,
|
||||
) -> Option<Vec<AppEvent>> {
|
||||
if let TagManagementState::Delete(index, confirm, choosing_yes) = &mut self.state {
|
||||
match key_event.code {
|
||||
KeyCode::Right => {
|
||||
if *confirm {
|
||||
*choosing_yes = false;
|
||||
} else {
|
||||
*index = if self.tags.is_empty() {
|
||||
0
|
||||
} else {
|
||||
(*index + 1) % self.tags.len()
|
||||
};
|
||||
}
|
||||
None
|
||||
}
|
||||
KeyCode::Left => {
|
||||
if *confirm {
|
||||
*choosing_yes = true;
|
||||
} else {
|
||||
*index = if *index == 0 {
|
||||
self.tags.len().saturating_sub(1)
|
||||
} else {
|
||||
*index - 1
|
||||
};
|
||||
}
|
||||
None
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if *confirm {
|
||||
if *choosing_yes && let Some(&(tag_id, _)) = self.tags.get(*index) {
|
||||
db.delete_tag(tag_id).expect("Database error");
|
||||
self.state = TagManagementState::Undecided(false);
|
||||
} else if !*choosing_yes {
|
||||
self.state = TagManagementState::Delete(*index, false, false)
|
||||
}
|
||||
} else {
|
||||
self.state = TagManagementState::Delete(*index, true, false)
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use ratatui::crossterm::event::{KeyCode, KeyEvent};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue