This commit is contained in:
maxstrb 2025-09-24 22:24:50 +02:00
parent 234b17a8b0
commit 3681aa2a22
15 changed files with 1109 additions and 143 deletions

225
src/popup/day_details.rs Normal file
View 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
View 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
View 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
View 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
}
}
}