wat/src/app.rs

315 lines
12 KiB
Rust
Raw Normal View History

use std::process::Command;
2025-11-16 11:41:05 -05:00
use chrono::Utc;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use crate::state::{AppState, TimeItem};
use crate::config::Config;
pub enum Screen {
Main,
Help,
ConfigHelp,
2025-11-16 11:41:05 -05:00
NewEntry,
}
pub struct App {
pub state: AppState,
pub config: Config,
pub current_screen: Screen,
pub needs_redraw: bool,
2025-11-16 11:41:05 -05:00
pub new_entry_buffer: String,
pub new_entry_project: String,
pub new_entry_cursor: usize,
pub new_entry_mode: NewEntryMode, // Task or Project
}
pub enum NewEntryMode {
Task,
Project,
}
impl App {
pub fn new() -> anyhow::Result<Self> {
Ok(Self {
state: AppState::load()?,
config: Config::load()?,
current_screen: Screen::Main,
needs_redraw: true,
2025-11-16 11:41:05 -05:00
new_entry_buffer: String::new(),
new_entry_project: String::new(),
new_entry_cursor: 0,
new_entry_mode: NewEntryMode::Task,
})
}
pub fn handle_event(&mut self, event: Event) -> anyhow::Result<bool> {
match self.current_screen {
Screen::Main => self.handle_main_event(event),
Screen::Help => self.handle_help_event(event),
Screen::ConfigHelp => self.handle_config_help_event(event),
2025-11-16 11:41:05 -05:00
Screen::NewEntry => self.handle_new_entry_event(event),
}
}
fn handle_main_event(&mut self, event: Event) -> anyhow::Result<bool> {
match event {
Event::Key(KeyEvent {
code,
modifiers,
..
}) => match (code, modifiers) {
(KeyCode::Char('q'), _) => return Ok(true),
(KeyCode::Char('j'), _) | (KeyCode::Down, _) => self.move_selection(1),
(KeyCode::Char('k'), _) | (KeyCode::Up, _) => self.move_selection(-1),
(KeyCode::Char('h'), _) | (KeyCode::Left, _) => self.change_pane(-1),
(KeyCode::Char('l'), _) | (KeyCode::Right, _) => self.change_pane(1),
2025-11-16 11:41:05 -05:00
(KeyCode::Char('n'), KeyModifiers::CONTROL) => self.change_pane(1),
(KeyCode::Char('p'), KeyModifiers::CONTROL) => self.change_pane(-1),
(KeyCode::Enter, _) => self.toggle_current_item()?,
(KeyCode::Char('e'), KeyModifiers::CONTROL) => self.edit_config()?,
(KeyCode::Char('?'), _) => self.current_screen = Screen::Help,
(KeyCode::Char('c'), _) => self.edit_app_config()?,
2025-11-16 11:41:05 -05:00
(KeyCode::Char('n'), _) => self.start_new_entry(),
_ => {}
},
_ => {}
}
Ok(false)
}
2025-11-16 11:41:05 -05:00
fn handle_new_entry_event(&mut self, event: Event) -> anyhow::Result<bool> {
match event {
Event::Key(KeyEvent {
code,
modifiers,
..
}) => {
match (code, modifiers) {
(KeyCode::Esc, _) => {
self.current_screen = Screen::Main;
self.new_entry_buffer.clear();
self.new_entry_project.clear();
self.new_entry_mode = NewEntryMode::Task;
}
(KeyCode::Enter, _) => {
match self.new_entry_mode {
NewEntryMode::Task => {
if !self.new_entry_buffer.is_empty() {
self.new_entry_mode = NewEntryMode::Project;
}
}
NewEntryMode::Project => {
if self.new_entry_project.is_empty() || self.config.is_valid_project(&self.new_entry_project) {
// Create new time item
let item = TimeItem {
name: self.new_entry_buffer.clone(),
tags: if self.new_entry_project.is_empty() {
vec![]
} else {
vec![self.new_entry_project.clone()]
},
last_used: Some(Utc::now()),
};
// Add to current pane (or recurring if in recent)
match self.state.current_pane {
0 => self.state.permanent_items.push(item), // Permanent Items
1 => self.state.recurring_items.push(item), // Recurring Items
2 => self.state.recent_items.push(item), // Ad-Hoc Items
_ => unreachable!(),
}
self.state.save()?;
// Clear and return to main screen
self.current_screen = Screen::Main;
self.new_entry_buffer.clear();
self.new_entry_project.clear();
self.new_entry_mode = NewEntryMode::Task;
}
}
}
}
(KeyCode::Backspace, _) => {
match self.new_entry_mode {
NewEntryMode::Task => {
if self.new_entry_cursor > 0 {
self.new_entry_buffer.remove(self.new_entry_cursor - 1);
self.new_entry_cursor -= 1;
}
}
NewEntryMode::Project => {
if self.new_entry_cursor > 0 {
self.new_entry_project.remove(self.new_entry_cursor - 1);
self.new_entry_cursor -= 1;
}
}
}
}
(KeyCode::Char(c), m) if m.is_empty() => {
match self.new_entry_mode {
NewEntryMode::Task => {
self.new_entry_buffer.insert(self.new_entry_cursor, c);
self.new_entry_cursor += 1;
}
NewEntryMode::Project => {
self.new_entry_project.insert(self.new_entry_cursor, c);
self.new_entry_cursor += 1;
}
}
}
_ => {}
}
}
_ => {}
}
Ok(false)
}
fn handle_help_event(&mut self, event: Event) -> anyhow::Result<bool> {
match event {
Event::Key(KeyEvent { code, .. }) => match code {
KeyCode::Char('c') => self.current_screen = Screen::ConfigHelp,
KeyCode::Esc | KeyCode::Char('q') => self.current_screen = Screen::Main,
_ => {}
},
_ => {}
}
Ok(false)
}
fn handle_config_help_event(&mut self, event: Event) -> anyhow::Result<bool> {
match event {
Event::Key(KeyEvent { code, .. }) => match code {
KeyCode::Esc | KeyCode::Char('q') => self.current_screen = Screen::Help,
_ => {}
},
_ => {}
}
Ok(false)
}
fn move_selection(&mut self, delta: i32) {
let items_len = match self.state.current_pane {
0 => self.state.permanent_items.len(),
1 => self.state.recurring_items.len(),
2 => self.state.recent_items.len(),
_ => return,
};
if items_len == 0 {
return;
}
let current = self.state.selected_indices[self.state.current_pane] as i32;
let new_index = (current + delta).rem_euclid(items_len as i32) as usize;
self.state.selected_indices[self.state.current_pane] = new_index;
}
fn change_pane(&mut self, delta: i32) {
self.state.current_pane = ((self.state.current_pane as i32 + delta).rem_euclid(3)) as usize;
}
fn get_current_item(&self) -> Option<TimeItem> {
let items = match self.state.current_pane {
0 => &self.state.permanent_items,
1 => &self.state.recurring_items,
2 => &self.state.recent_items,
_ => return None,
};
let index = self.state.selected_indices[self.state.current_pane];
items.get(index).cloned()
}
fn toggle_current_item(&mut self) -> anyhow::Result<()> {
if let Some(item) = self.get_current_item() {
if self.state.active_timer.as_ref().map(|(active, _)| active.name == item.name).unwrap_or(false) {
self.state.stop_timer()?;
} else {
self.state.start_timer(item)?;
}
self.state.save()?;
}
Ok(())
}
2025-11-16 11:41:05 -05:00
fn start_new_entry(&mut self) {
self.current_screen = Screen::NewEntry;
self.new_entry_buffer.clear();
self.new_entry_project.clear();
self.new_entry_cursor = 0;
self.new_entry_mode = NewEntryMode::Task;
}
fn edit_app_config(&mut self) -> anyhow::Result<()> {
use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode},
execute,
terminal::{LeaveAlternateScreen, EnterAlternateScreen},
};
use std::io::stdout;
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string());
let config_path = Config::config_path()?;
// Leave TUI mode
disable_raw_mode()?;
execute!(stdout(), LeaveAlternateScreen)?;
// Run editor
let status = Command::new(editor)
.arg(&config_path)
.status()?;
if status.success() {
// Reload entire application state
self.config = Config::load()?;
// Signal for complete reload
self.current_screen = Screen::Main;
self.needs_redraw = true;
}
// Return to TUI mode
execute!(stdout(), EnterAlternateScreen)?;
enable_raw_mode()?;
Ok(())
}
fn edit_config(&mut self) -> anyhow::Result<()> {
use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode},
execute,
terminal::{LeaveAlternateScreen, EnterAlternateScreen},
};
use std::io::stdout;
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string());
let config_path = AppState::config_file()?;
if !config_path.exists() {
std::fs::write(&config_path, "# WAT Configuration\n# Add your permanent items here\n\npermanent_items:\n - name: Daily Standup\n tags: [daily, meeting]\n")?;
}
// Leave TUI mode
disable_raw_mode()?;
execute!(stdout(), LeaveAlternateScreen)?;
// Run editor
let status = Command::new(editor)
.arg(&config_path)
.status()?;
if status.success() {
// Reload entire application state
self.state = AppState::load()?;
// Signal for complete reload
self.current_screen = Screen::Main;
self.needs_redraw = true;
}
// Return to TUI mode
execute!(stdout(), EnterAlternateScreen)?;
enable_raw_mode()?;
Ok(())
}
}