use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use enum_dispatch::enum_dispatch;
use ratatui::{
buffer::Buffer,
layout::{Constraint, Layout, Rect},
style::{Style, Stylize},
symbols::border,
text::Line,
widgets::{Block, Clear, Padding, Paragraph, Widget},
};
use uuid::Uuid;
use crate::store::Store;
#[enum_dispatch]
pub trait ModalControl {
fn render(&self, area: Rect, buf: &mut Buffer);
fn handle_keys(&mut self, key_event: KeyEvent, store: &mut Store);
fn closed(&self) -> bool;
}
#[derive(Debug)]
#[enum_dispatch(ModalControl)]
pub enum Modal {
EditTaskModal,
}
#[derive(Debug, Default, Clone, Copy, PartialEq)]
enum Field {
#[default]
Body,
Context,
}
impl Field {
fn toggle(self) -> Self {
match self {
Field::Body => Field::Context,
Field::Context => Field::Body,
}
}
}
#[derive(Debug, Default)]
pub struct EditTaskModal {
task_id: Uuid,
body: String,
context: String,
focus: Field,
closed: bool,
}
impl ModalControl for EditTaskModal {
fn render(&self, area: Rect, buf: &mut Buffer) {
let popup_area = area.centered(Constraint::Percentage(70), Constraint::Length(11));
let title = if self.task_id == Uuid::nil() {
" Add task "
} else {
" Edit task "
};
Clear.render(popup_area, buf);
let [body_area, context_area, ui_hint_area] = Layout::vertical([
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(1),
])
.areas(popup_area);
self.render_field(body_area, buf, title, &self.body, Field::Body);
self.render_field(
context_area,
buf,
" Context ",
&self.context,
Field::Context,
);
Paragraph::new(
Line::from(" [Tab] [Enter] [Esc] ")
.style(Style::new().blue().bold())
.centered(),
)
.render(ui_hint_area, buf);
}
fn handle_keys(&mut self, key_event: KeyEvent, store: &mut Store) {
let KeyEvent {
code, modifiers, ..
} = key_event;
if modifiers == KeyModifiers::CONTROL {
match code {
KeyCode::Char('w') => {
let s = self.focused_input_mut();
s.truncate(s.trim_end().len());
s.drain(s.rfind(' ').map_or(0, |p| p + 1)..);
}
KeyCode::Char('u') => self.focused_input_mut().clear(),
_ => {}
}
return;
}
match code {
KeyCode::Tab => self.focus = self.focus.toggle(),
KeyCode::Enter => {
let body = self.body.trim().to_string();
if !body.is_empty() {
let context = self.context.trim();
store.upsert(self.task_id, body, context);
}
self.closed = true;
}
KeyCode::Esc => self.closed = true,
KeyCode::Backspace => {
self.focused_input_mut().pop();
}
KeyCode::Char(c) => self.focused_input_mut().push(c),
_ => {}
}
}
fn closed(&self) -> bool {
self.closed
}
}
impl EditTaskModal {
pub fn new_task(context: impl Into<String>) -> Modal {
let context = context.into();
Modal::EditTaskModal(Self {
context,
..Default::default()
})
}
pub fn edit_task(task_id: Uuid, store: &Store) -> Modal {
let Some(task) = store.find_task(task_id) else {
panic!("task with id {task_id} not found in store")
};
Modal::EditTaskModal(Self {
task_id: task.id,
body: task.body.clone(),
context: task.context.clone(),
..Default::default()
})
}
fn focused_input_mut(&mut self) -> &mut String {
match self.focus {
Field::Body => &mut self.body,
Field::Context => &mut self.context,
}
}
fn render_field(&self, area: Rect, buf: &mut Buffer, title: &str, value: &str, field: Field) {
let mut spans = vec![value.to_string().into()];
let border_style = if self.focus == field {
spans.push("█".into());
Style::new().blue()
} else {
Style::new().dim()
};
let block = Block::bordered()
.title(title)
.border_set(border::THICK)
.padding(Padding::uniform(1))
.border_style(border_style);
Paragraph::new(Line::from(spans).white())
.block(block)
.render(area, buf);
}
}