use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
layout::{Alignment, Constraint, Layout},
style::{Modifier, Style},
text::{Line, Span},
widgets::{Clear, Paragraph, Wrap},
Frame,
};
use super::{centered_rect, dialog_block, hint_style, DialogAction};
use crate::ui::theme;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DeleteProjectChoice {
#[default]
MoveToInbox,
DeleteTasks,
Cancel,
}
#[derive(Debug)]
pub struct DeleteProjectDialog {
pub project_id: String,
pub project_name: String,
pub task_count: usize,
pub selected: DeleteProjectChoice,
}
impl DeleteProjectDialog {
pub fn new(project_id: String, project_name: String, task_count: usize) -> Self {
Self {
project_id,
project_name,
task_count,
selected: DeleteProjectChoice::MoveToInbox,
}
}
pub fn choice(&self) -> DeleteProjectChoice {
self.selected
}
pub fn handle_key(&mut self, key: KeyEvent) -> DialogAction {
match key.code {
KeyCode::Char('m') | KeyCode::Char('M') => {
self.selected = DeleteProjectChoice::MoveToInbox;
DialogAction::Submit
}
KeyCode::Char('d') | KeyCode::Char('D') => {
self.selected = DeleteProjectChoice::DeleteTasks;
DialogAction::Submit
}
KeyCode::Esc | KeyCode::Char('c') | KeyCode::Char('C') => {
self.selected = DeleteProjectChoice::Cancel;
DialogAction::Cancel
}
KeyCode::Up | KeyCode::Char('k') => {
self.selected = match self.selected {
DeleteProjectChoice::MoveToInbox => DeleteProjectChoice::Cancel,
DeleteProjectChoice::DeleteTasks => DeleteProjectChoice::MoveToInbox,
DeleteProjectChoice::Cancel => DeleteProjectChoice::DeleteTasks,
};
DialogAction::None
}
KeyCode::Down | KeyCode::Char('j') => {
self.selected = match self.selected {
DeleteProjectChoice::MoveToInbox => DeleteProjectChoice::DeleteTasks,
DeleteProjectChoice::DeleteTasks => DeleteProjectChoice::Cancel,
DeleteProjectChoice::Cancel => DeleteProjectChoice::MoveToInbox,
};
DialogAction::None
}
KeyCode::Enter => {
if self.selected == DeleteProjectChoice::Cancel {
DialogAction::Cancel
} else {
DialogAction::Submit
}
}
_ => DialogAction::None,
}
}
pub fn render(&self, frame: &mut Frame) {
let area = frame.area();
let dialog_width = 55.min(area.width.saturating_sub(4));
let dialog_height = 14.min(area.height.saturating_sub(4));
let dialog_area = centered_rect(dialog_width, dialog_height, area);
frame.render_widget(Clear, area);
frame.render_widget(
Paragraph::new("").style(Style::default().bg(theme::BG_DARK)),
area,
);
let block = dialog_block("Delete Project", true);
let inner = block.inner(dialog_area);
frame.render_widget(block, dialog_area);
let chunks = Layout::vertical([
Constraint::Length(2), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), ])
.split(inner);
let task_text = if self.task_count == 1 {
"1 task".to_string()
} else {
format!("{} tasks", self.task_count)
};
let message = format!(
"Delete project \"{}\"?\nThis project has {}.",
self.project_name, task_text
);
let message_widget = Paragraph::new(message)
.style(Style::default().fg(theme::TEXT_PRIMARY))
.alignment(Alignment::Center)
.wrap(Wrap { trim: true });
frame.render_widget(message_widget, chunks[0]);
let options = [
(DeleteProjectChoice::MoveToInbox, "Move tasks to Inbox", "m", theme::SUCCESS),
(DeleteProjectChoice::DeleteTasks, "Delete all tasks", "d", theme::ERROR),
(DeleteProjectChoice::Cancel, "Cancel", "c", theme::TEXT_MUTED),
];
for (i, (choice, label, key, color)) in options.iter().enumerate() {
let is_selected = self.selected == *choice;
let style = if is_selected {
Style::default()
.fg(theme::TEXT_PRIMARY)
.bg(*color)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(theme::TEXT_MUTED)
};
let key_style = Style::default()
.fg(*color)
.add_modifier(Modifier::BOLD);
let prefix = if is_selected { ">" } else { " " };
let line = Line::from(vec![
Span::styled(format!(" {} ", prefix), style),
Span::styled(format!("[{}] ", key), key_style),
Span::styled(format!("{} ", label), style),
]);
let option = Paragraph::new(line).alignment(Alignment::Center);
frame.render_widget(option, chunks[2 + i]);
}
let hint = Paragraph::new("j/k to navigate, Enter to confirm")
.style(hint_style())
.alignment(Alignment::Center);
frame.render_widget(hint, chunks[6]);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::KeyModifiers;
#[test]
fn test_new_dialog() {
let dialog = DeleteProjectDialog::new(
"proj-1".to_string(),
"Work".to_string(),
5,
);
assert_eq!(dialog.project_id, "proj-1");
assert_eq!(dialog.project_name, "Work");
assert_eq!(dialog.task_count, 5);
assert_eq!(dialog.selected, DeleteProjectChoice::MoveToInbox);
}
#[test]
fn test_move_shortcut() {
let mut dialog = DeleteProjectDialog::new(
"proj-1".to_string(),
"Work".to_string(),
5,
);
let key = KeyEvent::new(KeyCode::Char('m'), KeyModifiers::NONE);
let action = dialog.handle_key(key);
assert_eq!(action, DialogAction::Submit);
assert_eq!(dialog.selected, DeleteProjectChoice::MoveToInbox);
}
#[test]
fn test_delete_shortcut() {
let mut dialog = DeleteProjectDialog::new(
"proj-1".to_string(),
"Work".to_string(),
5,
);
let key = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE);
let action = dialog.handle_key(key);
assert_eq!(action, DialogAction::Submit);
assert_eq!(dialog.selected, DeleteProjectChoice::DeleteTasks);
}
#[test]
fn test_cancel_shortcut() {
let mut dialog = DeleteProjectDialog::new(
"proj-1".to_string(),
"Work".to_string(),
5,
);
let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
let action = dialog.handle_key(key);
assert_eq!(action, DialogAction::Cancel);
}
#[test]
fn test_navigation() {
let mut dialog = DeleteProjectDialog::new(
"proj-1".to_string(),
"Work".to_string(),
5,
);
assert_eq!(dialog.selected, DeleteProjectChoice::MoveToInbox);
let key = KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
dialog.handle_key(key);
assert_eq!(dialog.selected, DeleteProjectChoice::DeleteTasks);
dialog.handle_key(key);
assert_eq!(dialog.selected, DeleteProjectChoice::Cancel);
let key = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
dialog.handle_key(key);
assert_eq!(dialog.selected, DeleteProjectChoice::DeleteTasks);
}
}