2025-11-16 13:12:45 -05:00
|
|
|
use crate::config::Config;
|
|
|
|
|
use crate::state::{AppState, TimeItem};
|
2025-11-16 11:41:05 -05:00
|
|
|
use chrono::Utc;
|
2025-11-16 09:57:01 -05:00
|
|
|
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
2025-11-22 11:27:01 -05:00
|
|
|
use serde::Deserialize;
|
2025-11-16 13:12:45 -05:00
|
|
|
use std::process::Command;
|
2025-11-16 10:19:14 -05:00
|
|
|
|
2025-11-22 11:27:01 -05:00
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
|
|
|
pub(crate) struct WatsonFrame {
|
|
|
|
|
id: String,
|
|
|
|
|
project: String,
|
|
|
|
|
start: String,
|
|
|
|
|
stop: String,
|
|
|
|
|
tags: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-16 10:19:14 -05:00
|
|
|
pub enum Screen {
|
|
|
|
|
Main,
|
|
|
|
|
Help,
|
|
|
|
|
ConfigHelp,
|
2025-11-16 11:41:05 -05:00
|
|
|
NewEntry,
|
2025-11-22 10:45:10 -05:00
|
|
|
ReassignProject,
|
2025-11-22 11:12:46 -05:00
|
|
|
LogView,
|
2025-11-22 11:57:28 -05:00
|
|
|
LogViewHelp,
|
2025-11-22 11:12:46 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub enum LogViewPeriod {
|
|
|
|
|
Day,
|
|
|
|
|
Week,
|
|
|
|
|
Month,
|
2025-11-16 10:19:14 -05:00
|
|
|
}
|
2025-11-16 09:57:01 -05:00
|
|
|
|
2025-11-22 11:53:43 -05:00
|
|
|
pub enum LogViewGrouping {
|
|
|
|
|
ByDate,
|
|
|
|
|
ByProject,
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-23 12:01:13 -05:00
|
|
|
pub enum LogViewSelection {
|
|
|
|
|
Entry, // Individual entry selected
|
|
|
|
|
Project, // Whole project selected (ByProject mode only)
|
|
|
|
|
Day, // Whole day selected
|
2025-11-23 12:05:10 -05:00
|
|
|
All, // Entire view/period selected
|
2025-11-23 12:01:13 -05:00
|
|
|
}
|
|
|
|
|
|
2025-11-16 09:57:01 -05:00
|
|
|
pub struct App {
|
|
|
|
|
pub state: AppState,
|
2025-11-16 10:19:14 -05:00
|
|
|
pub config: Config,
|
|
|
|
|
pub current_screen: Screen,
|
2025-11-22 11:12:46 -05:00
|
|
|
pub needs_clear: bool,
|
2025-11-16 11:41:05 -05:00
|
|
|
pub new_entry_buffer: String,
|
|
|
|
|
pub new_entry_project: String,
|
|
|
|
|
pub new_entry_cursor: usize,
|
2025-11-16 13:12:45 -05:00
|
|
|
pub new_entry_mode: NewEntryMode, // Task or Project
|
2025-11-22 10:45:10 -05:00
|
|
|
pub reassign_project_buffer: String,
|
|
|
|
|
pub reassign_project_cursor: usize,
|
2025-11-22 11:12:46 -05:00
|
|
|
pub log_view_period: LogViewPeriod,
|
2025-11-22 11:53:43 -05:00
|
|
|
pub log_view_grouping: LogViewGrouping,
|
2025-11-23 12:01:13 -05:00
|
|
|
pub log_view_selection_level: LogViewSelection,
|
2025-11-22 11:27:01 -05:00
|
|
|
pub log_view_frames: Vec<WatsonFrame>,
|
2025-11-22 11:12:46 -05:00
|
|
|
pub log_view_content: Vec<String>,
|
|
|
|
|
pub log_view_scroll: usize,
|
2025-11-22 11:27:01 -05:00
|
|
|
pub log_view_selected: usize,
|
2025-11-23 12:21:03 -05:00
|
|
|
pub help_scroll: usize,
|
2025-11-22 11:35:49 -05:00
|
|
|
pub clipboard: Option<arboard::Clipboard>,
|
2025-11-16 12:25:22 -05:00
|
|
|
pub status_message: Option<(String, std::time::Instant)>,
|
2025-11-16 11:41:05 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub enum NewEntryMode {
|
|
|
|
|
Task,
|
|
|
|
|
Project,
|
2025-11-16 09:57:01 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl App {
|
2025-11-23 12:01:13 -05:00
|
|
|
// Helper to determine if a line is an entry line based on grouping mode
|
|
|
|
|
fn is_entry_line(&self, line: &str) -> bool {
|
|
|
|
|
match self.log_view_grouping {
|
|
|
|
|
LogViewGrouping::ByProject => line.starts_with("\t\t"),
|
|
|
|
|
LogViewGrouping::ByDate => line.starts_with('\t') && !line.starts_with("\t\t"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-16 09:57:01 -05:00
|
|
|
pub fn new() -> anyhow::Result<Self> {
|
|
|
|
|
Ok(Self {
|
|
|
|
|
state: AppState::load()?,
|
2025-11-16 10:19:14 -05:00
|
|
|
config: Config::load()?,
|
|
|
|
|
current_screen: Screen::Main,
|
2025-11-22 11:12:46 -05:00
|
|
|
needs_clear: false,
|
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,
|
2025-11-22 10:45:10 -05:00
|
|
|
reassign_project_buffer: String::new(),
|
|
|
|
|
reassign_project_cursor: 0,
|
2025-11-22 11:12:46 -05:00
|
|
|
log_view_period: LogViewPeriod::Day,
|
2025-11-22 11:53:43 -05:00
|
|
|
log_view_grouping: LogViewGrouping::ByDate,
|
2025-11-23 12:01:13 -05:00
|
|
|
log_view_selection_level: LogViewSelection::Entry,
|
2025-11-22 11:27:01 -05:00
|
|
|
log_view_frames: Vec::new(),
|
2025-11-22 11:12:46 -05:00
|
|
|
log_view_content: Vec::new(),
|
|
|
|
|
log_view_scroll: 0,
|
2025-11-22 11:27:01 -05:00
|
|
|
log_view_selected: 0,
|
2025-11-23 12:21:03 -05:00
|
|
|
help_scroll: 0,
|
2025-11-22 11:35:49 -05:00
|
|
|
clipboard: arboard::Clipboard::new().ok(),
|
2025-11-16 12:25:22 -05:00
|
|
|
status_message: None,
|
2025-11-16 09:57:01 -05:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn handle_event(&mut self, event: Event) -> anyhow::Result<bool> {
|
2025-11-16 12:25:22 -05:00
|
|
|
// Update status message
|
|
|
|
|
self.update_status_message();
|
2025-11-16 13:12:45 -05:00
|
|
|
|
2025-11-22 11:12:46 -05:00
|
|
|
let previous_screen = std::mem::discriminant(&self.current_screen);
|
|
|
|
|
|
|
|
|
|
let result = match self.current_screen {
|
2025-11-16 10:19:14 -05:00
|
|
|
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),
|
2025-11-22 10:45:10 -05:00
|
|
|
Screen::ReassignProject => self.handle_reassign_project_event(event),
|
2025-11-22 11:12:46 -05:00
|
|
|
Screen::LogView => self.handle_log_view_event(event),
|
2025-11-22 11:57:28 -05:00
|
|
|
Screen::LogViewHelp => self.handle_log_view_help_event(event),
|
2025-11-22 11:12:46 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// If we switched screens, signal that we need to clear
|
|
|
|
|
let current_screen = std::mem::discriminant(&self.current_screen);
|
|
|
|
|
if previous_screen != current_screen {
|
|
|
|
|
self.needs_clear = true;
|
2025-11-16 10:19:14 -05:00
|
|
|
}
|
2025-11-22 11:12:46 -05:00
|
|
|
|
|
|
|
|
result
|
2025-11-16 10:19:14 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn handle_main_event(&mut self, event: Event) -> anyhow::Result<bool> {
|
2025-11-16 09:57:01 -05:00
|
|
|
match event {
|
|
|
|
|
Event::Key(KeyEvent {
|
2025-11-16 13:12:45 -05:00
|
|
|
code, modifiers, ..
|
2025-11-16 09:57:01 -05:00
|
|
|
}) => 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),
|
2025-11-16 09:57:01 -05:00
|
|
|
(KeyCode::Enter, _) => self.toggle_current_item()?,
|
|
|
|
|
(KeyCode::Char('e'), KeyModifiers::CONTROL) => self.edit_config()?,
|
2025-11-23 12:21:03 -05:00
|
|
|
(KeyCode::Char('?'), _) => {
|
|
|
|
|
self.help_scroll = 0;
|
|
|
|
|
self.current_screen = Screen::Help;
|
|
|
|
|
}
|
2025-11-16 10:19:14 -05:00
|
|
|
(KeyCode::Char('c'), _) => self.edit_app_config()?,
|
2025-11-16 11:41:05 -05:00
|
|
|
(KeyCode::Char('n'), _) => self.start_new_entry(),
|
2025-11-22 10:45:10 -05:00
|
|
|
(KeyCode::Char('p'), _) => self.start_reassign_project(),
|
2025-11-22 11:12:46 -05:00
|
|
|
(KeyCode::Char('v'), _) => self.start_log_view()?,
|
2025-11-16 12:25:22 -05:00
|
|
|
(KeyCode::Char('d'), _) => self.delete_current_item()?,
|
2025-11-16 10:19:14 -05:00
|
|
|
_ => {}
|
|
|
|
|
},
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
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 {
|
2025-11-16 13:12:45 -05:00
|
|
|
code, modifiers, ..
|
2025-11-16 11:41:05 -05:00
|
|
|
}) => {
|
|
|
|
|
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;
|
2025-11-16 13:12:45 -05:00
|
|
|
self.new_entry_cursor = 0;
|
2025-11-16 11:41:05 -05:00
|
|
|
}
|
|
|
|
|
(KeyCode::Enter, _) => {
|
|
|
|
|
match self.new_entry_mode {
|
|
|
|
|
NewEntryMode::Task => {
|
2025-11-22 11:53:43 -05:00
|
|
|
// Move from Tag to Project
|
|
|
|
|
self.new_entry_mode = NewEntryMode::Project;
|
|
|
|
|
self.new_entry_cursor = self.new_entry_buffer.len();
|
2025-11-16 11:41:05 -05:00
|
|
|
}
|
|
|
|
|
NewEntryMode::Project => {
|
2025-11-22 11:53:43 -05:00
|
|
|
// Project is required, tag is optional
|
|
|
|
|
if !self.new_entry_buffer.is_empty() {
|
2025-11-16 11:41:05 -05:00
|
|
|
// 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 {
|
2025-11-16 13:12:45 -05:00
|
|
|
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
|
2025-11-16 11:41:05 -05:00
|
|
|
_ => unreachable!(),
|
|
|
|
|
}
|
|
|
|
|
self.state.save()?;
|
2025-11-16 13:12:45 -05:00
|
|
|
|
2025-11-16 11:41:05 -05:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-16 13:12:45 -05:00
|
|
|
(KeyCode::Backspace, _) => match self.new_entry_mode {
|
|
|
|
|
NewEntryMode::Task => {
|
|
|
|
|
if self.new_entry_cursor > 0 {
|
2025-11-22 11:53:43 -05:00
|
|
|
self.new_entry_project.remove(self.new_entry_cursor - 1);
|
2025-11-16 13:12:45 -05:00
|
|
|
self.new_entry_cursor -= 1;
|
2025-11-16 11:41:05 -05:00
|
|
|
}
|
2025-11-16 13:12:45 -05:00
|
|
|
}
|
|
|
|
|
NewEntryMode::Project => {
|
|
|
|
|
if self.new_entry_cursor > 0 {
|
|
|
|
|
let idx = self.new_entry_cursor - 1;
|
2025-11-22 11:53:43 -05:00
|
|
|
if idx < self.new_entry_buffer.len() {
|
|
|
|
|
self.new_entry_buffer.remove(idx);
|
2025-11-16 11:41:05 -05:00
|
|
|
self.new_entry_cursor -= 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-16 13:12:45 -05:00
|
|
|
},
|
|
|
|
|
(KeyCode::Char(c), m) if m.is_empty() => match self.new_entry_mode {
|
|
|
|
|
NewEntryMode::Task => {
|
2025-11-22 11:53:43 -05:00
|
|
|
self.new_entry_project.insert(self.new_entry_cursor, c);
|
2025-11-16 13:12:45 -05:00
|
|
|
self.new_entry_cursor += 1;
|
|
|
|
|
}
|
|
|
|
|
NewEntryMode::Project => {
|
2025-11-22 11:53:43 -05:00
|
|
|
if self.new_entry_cursor <= self.new_entry_buffer.len() {
|
|
|
|
|
self.new_entry_buffer.insert(self.new_entry_cursor, c);
|
2025-11-16 11:41:05 -05:00
|
|
|
self.new_entry_cursor += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-16 13:12:45 -05:00
|
|
|
},
|
2025-11-16 11:41:05 -05:00
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
Ok(false)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-16 10:19:14 -05:00
|
|
|
fn handle_help_event(&mut self, event: Event) -> anyhow::Result<bool> {
|
|
|
|
|
match event {
|
|
|
|
|
Event::Key(KeyEvent { code, .. }) => match code {
|
2025-11-23 12:21:03 -05:00
|
|
|
KeyCode::Char('c') => {
|
|
|
|
|
self.help_scroll = 0;
|
|
|
|
|
self.current_screen = Screen::ConfigHelp;
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Char('j') | KeyCode::Down => {
|
|
|
|
|
self.help_scroll = self.help_scroll.saturating_add(1);
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Char('k') | KeyCode::Up => {
|
|
|
|
|
self.help_scroll = self.help_scroll.saturating_sub(1);
|
|
|
|
|
}
|
|
|
|
|
KeyCode::PageDown => {
|
|
|
|
|
self.help_scroll = self.help_scroll.saturating_add(10);
|
|
|
|
|
}
|
|
|
|
|
KeyCode::PageUp => {
|
|
|
|
|
self.help_scroll = self.help_scroll.saturating_sub(10);
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('?') => {
|
|
|
|
|
self.help_scroll = 0;
|
|
|
|
|
self.current_screen = Screen::Main;
|
|
|
|
|
}
|
2025-11-16 10:19:14 -05:00
|
|
|
_ => {}
|
|
|
|
|
},
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
Ok(false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn handle_config_help_event(&mut self, event: Event) -> anyhow::Result<bool> {
|
|
|
|
|
match event {
|
|
|
|
|
Event::Key(KeyEvent { code, .. }) => match code {
|
2025-11-23 12:21:03 -05:00
|
|
|
KeyCode::Char('j') | KeyCode::Down => {
|
|
|
|
|
self.help_scroll = self.help_scroll.saturating_add(1);
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Char('k') | KeyCode::Up => {
|
|
|
|
|
self.help_scroll = self.help_scroll.saturating_sub(1);
|
|
|
|
|
}
|
|
|
|
|
KeyCode::PageDown => {
|
|
|
|
|
self.help_scroll = self.help_scroll.saturating_add(10);
|
|
|
|
|
}
|
|
|
|
|
KeyCode::PageUp => {
|
|
|
|
|
self.help_scroll = self.help_scroll.saturating_sub(10);
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('?') => {
|
|
|
|
|
self.help_scroll = 0;
|
|
|
|
|
self.current_screen = Screen::Help;
|
|
|
|
|
}
|
2025-11-16 09:57:01 -05:00
|
|
|
_ => {}
|
|
|
|
|
},
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
Ok(false)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-22 11:57:28 -05:00
|
|
|
fn handle_log_view_help_event(&mut self, event: Event) -> anyhow::Result<bool> {
|
|
|
|
|
match event {
|
|
|
|
|
Event::Key(KeyEvent { code, .. }) => match code {
|
2025-11-23 12:21:03 -05:00
|
|
|
KeyCode::Char('j') | KeyCode::Down => {
|
|
|
|
|
self.help_scroll = self.help_scroll.saturating_add(1);
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Char('k') | KeyCode::Up => {
|
|
|
|
|
self.help_scroll = self.help_scroll.saturating_sub(1);
|
|
|
|
|
}
|
|
|
|
|
KeyCode::PageDown => {
|
|
|
|
|
self.help_scroll = self.help_scroll.saturating_add(10);
|
|
|
|
|
}
|
|
|
|
|
KeyCode::PageUp => {
|
|
|
|
|
self.help_scroll = self.help_scroll.saturating_sub(10);
|
|
|
|
|
}
|
2025-11-22 11:57:28 -05:00
|
|
|
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('?') => {
|
2025-11-23 12:21:03 -05:00
|
|
|
self.help_scroll = 0;
|
2025-11-22 11:57:28 -05:00
|
|
|
self.current_screen = Screen::LogView;
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
},
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
Ok(false)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-22 10:45:10 -05:00
|
|
|
fn handle_reassign_project_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.reassign_project_buffer.clear();
|
|
|
|
|
self.reassign_project_cursor = 0;
|
|
|
|
|
}
|
|
|
|
|
(KeyCode::Enter, _) => {
|
|
|
|
|
if self.reassign_project_buffer.is_empty()
|
|
|
|
|
|| self.config.is_valid_project(&self.reassign_project_buffer)
|
|
|
|
|
{
|
|
|
|
|
self.reassign_project_for_current_item()?;
|
|
|
|
|
self.current_screen = Screen::Main;
|
|
|
|
|
self.reassign_project_buffer.clear();
|
|
|
|
|
self.reassign_project_cursor = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
(KeyCode::Backspace, _) => {
|
|
|
|
|
if self.reassign_project_cursor > 0 {
|
|
|
|
|
let idx = self.reassign_project_cursor - 1;
|
|
|
|
|
if idx < self.reassign_project_buffer.len() {
|
|
|
|
|
self.reassign_project_buffer.remove(idx);
|
|
|
|
|
self.reassign_project_cursor -= 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
(KeyCode::Char(c), m) if m.is_empty() => {
|
|
|
|
|
if self.reassign_project_cursor <= self.reassign_project_buffer.len() {
|
|
|
|
|
self.reassign_project_buffer.insert(self.reassign_project_cursor, c);
|
|
|
|
|
self.reassign_project_cursor += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
Ok(false)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-22 11:12:46 -05:00
|
|
|
fn handle_log_view_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::Main;
|
|
|
|
|
self.log_view_scroll = 0;
|
2025-11-22 11:27:01 -05:00
|
|
|
self.log_view_selected = 0;
|
2025-11-22 11:12:46 -05:00
|
|
|
self.needs_clear = true;
|
|
|
|
|
}
|
2025-11-22 11:57:28 -05:00
|
|
|
KeyCode::Char('?') => {
|
2025-11-23 12:21:03 -05:00
|
|
|
self.help_scroll = 0;
|
2025-11-22 11:57:28 -05:00
|
|
|
self.current_screen = Screen::LogViewHelp;
|
|
|
|
|
}
|
2025-11-22 11:12:46 -05:00
|
|
|
KeyCode::Char('d') => {
|
|
|
|
|
self.log_view_period = LogViewPeriod::Day;
|
|
|
|
|
self.log_view_scroll = 0;
|
2025-11-22 11:27:01 -05:00
|
|
|
self.log_view_selected = 0;
|
2025-11-23 12:01:13 -05:00
|
|
|
self.log_view_selection_level = LogViewSelection::Entry;
|
2025-11-22 11:12:46 -05:00
|
|
|
self.load_log_content()?;
|
|
|
|
|
self.needs_clear = true;
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Char('w') => {
|
|
|
|
|
self.log_view_period = LogViewPeriod::Week;
|
|
|
|
|
self.log_view_scroll = 0;
|
2025-11-22 11:27:01 -05:00
|
|
|
self.log_view_selected = 0;
|
2025-11-23 12:01:13 -05:00
|
|
|
self.log_view_selection_level = LogViewSelection::Entry;
|
2025-11-22 11:12:46 -05:00
|
|
|
self.load_log_content()?;
|
|
|
|
|
self.needs_clear = true;
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Char('m') => {
|
|
|
|
|
self.log_view_period = LogViewPeriod::Month;
|
|
|
|
|
self.log_view_scroll = 0;
|
2025-11-22 11:27:01 -05:00
|
|
|
self.log_view_selected = 0;
|
2025-11-23 12:01:13 -05:00
|
|
|
self.log_view_selection_level = LogViewSelection::Entry;
|
2025-11-22 11:12:46 -05:00
|
|
|
self.load_log_content()?;
|
|
|
|
|
self.needs_clear = true;
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Char('j') | KeyCode::Down => {
|
2025-11-23 12:01:13 -05:00
|
|
|
match self.log_view_selection_level {
|
|
|
|
|
LogViewSelection::Entry => {
|
|
|
|
|
// Move to next entry
|
|
|
|
|
if self.log_view_selected < self.log_view_frames.len().saturating_sub(1) {
|
|
|
|
|
self.log_view_selected += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
LogViewSelection::Project => {
|
|
|
|
|
// Jump to next project group
|
|
|
|
|
self.jump_to_next_project();
|
|
|
|
|
}
|
|
|
|
|
LogViewSelection::Day => {
|
|
|
|
|
// Jump to next day
|
|
|
|
|
self.jump_to_next_day();
|
|
|
|
|
}
|
2025-11-23 12:05:10 -05:00
|
|
|
LogViewSelection::All => {
|
|
|
|
|
// No navigation at All level - already selecting everything
|
|
|
|
|
}
|
2025-11-22 11:12:46 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Char('k') | KeyCode::Up => {
|
2025-11-23 12:01:13 -05:00
|
|
|
match self.log_view_selection_level {
|
|
|
|
|
LogViewSelection::Entry => {
|
|
|
|
|
// Move to previous entry
|
|
|
|
|
if self.log_view_selected > 0 {
|
|
|
|
|
self.log_view_selected -= 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
LogViewSelection::Project => {
|
|
|
|
|
// Jump to previous project group
|
|
|
|
|
self.jump_to_previous_project();
|
|
|
|
|
}
|
|
|
|
|
LogViewSelection::Day => {
|
|
|
|
|
// Jump to previous day
|
|
|
|
|
self.jump_to_previous_day();
|
|
|
|
|
}
|
2025-11-23 12:05:10 -05:00
|
|
|
LogViewSelection::All => {
|
|
|
|
|
// No navigation at All level - already selecting everything
|
|
|
|
|
}
|
2025-11-22 11:12:46 -05:00
|
|
|
}
|
|
|
|
|
}
|
2025-11-22 11:27:01 -05:00
|
|
|
KeyCode::Char('e') => {
|
2025-11-23 12:01:13 -05:00
|
|
|
// Only allow edit when selecting individual entry
|
|
|
|
|
if matches!(self.log_view_selection_level, LogViewSelection::Entry) {
|
|
|
|
|
self.edit_selected_frame()?;
|
|
|
|
|
}
|
2025-11-22 11:27:01 -05:00
|
|
|
}
|
|
|
|
|
KeyCode::Char('x') => {
|
2025-11-23 12:01:13 -05:00
|
|
|
// Only allow delete when selecting individual entry
|
|
|
|
|
if matches!(self.log_view_selection_level, LogViewSelection::Entry) {
|
|
|
|
|
self.delete_selected_frame()?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Char('h') | KeyCode::Left => {
|
|
|
|
|
// Zoom out selection level
|
|
|
|
|
self.log_view_selection_level = match (&self.log_view_selection_level, &self.log_view_grouping) {
|
2025-11-23 12:05:10 -05:00
|
|
|
(LogViewSelection::All, _) => LogViewSelection::All, // Already at highest
|
|
|
|
|
(LogViewSelection::Day, _) => LogViewSelection::All,
|
2025-11-23 12:01:13 -05:00
|
|
|
(LogViewSelection::Project, _) => LogViewSelection::Day,
|
|
|
|
|
(LogViewSelection::Entry, LogViewGrouping::ByProject) => LogViewSelection::Project,
|
|
|
|
|
(LogViewSelection::Entry, LogViewGrouping::ByDate) => LogViewSelection::Day,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Char('l') | KeyCode::Right => {
|
|
|
|
|
// Zoom in selection level
|
|
|
|
|
self.log_view_selection_level = match (&self.log_view_selection_level, &self.log_view_grouping) {
|
|
|
|
|
(LogViewSelection::Entry, _) => LogViewSelection::Entry, // Already at lowest
|
|
|
|
|
(LogViewSelection::Project, _) => LogViewSelection::Entry,
|
|
|
|
|
(LogViewSelection::Day, LogViewGrouping::ByProject) => LogViewSelection::Project,
|
|
|
|
|
(LogViewSelection::Day, LogViewGrouping::ByDate) => LogViewSelection::Entry,
|
2025-11-23 12:05:10 -05:00
|
|
|
(LogViewSelection::All, LogViewGrouping::ByProject) => LogViewSelection::Day,
|
|
|
|
|
(LogViewSelection::All, LogViewGrouping::ByDate) => LogViewSelection::Day,
|
2025-11-23 12:01:13 -05:00
|
|
|
};
|
2025-11-22 11:27:01 -05:00
|
|
|
}
|
2025-11-22 11:35:49 -05:00
|
|
|
KeyCode::Char('c') => {
|
|
|
|
|
self.copy_log_to_clipboard()?;
|
|
|
|
|
}
|
2025-11-22 11:53:43 -05:00
|
|
|
KeyCode::Char('g') => {
|
|
|
|
|
// Toggle grouping mode
|
|
|
|
|
self.log_view_grouping = match self.log_view_grouping {
|
|
|
|
|
LogViewGrouping::ByDate => LogViewGrouping::ByProject,
|
|
|
|
|
LogViewGrouping::ByProject => LogViewGrouping::ByDate,
|
|
|
|
|
};
|
|
|
|
|
self.log_view_selected = 0;
|
2025-11-23 12:01:13 -05:00
|
|
|
self.log_view_selection_level = LogViewSelection::Entry;
|
2025-11-22 11:53:43 -05:00
|
|
|
self.format_log_entries();
|
|
|
|
|
self.needs_clear = true;
|
|
|
|
|
}
|
2025-11-22 11:12:46 -05:00
|
|
|
KeyCode::PageDown => {
|
2025-11-22 11:27:01 -05:00
|
|
|
self.log_view_selected = (self.log_view_selected + 10)
|
|
|
|
|
.min(self.log_view_frames.len().saturating_sub(1));
|
2025-11-22 11:12:46 -05:00
|
|
|
}
|
|
|
|
|
KeyCode::PageUp => {
|
2025-11-22 11:27:01 -05:00
|
|
|
self.log_view_selected = self.log_view_selected.saturating_sub(10);
|
2025-11-22 11:12:46 -05:00
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
},
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
Ok(false)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-22 11:27:01 -05:00
|
|
|
fn format_log_entries(&mut self) {
|
|
|
|
|
if self.log_view_frames.is_empty() {
|
|
|
|
|
self.log_view_content = vec!["No log entries for this period.".to_string()];
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-22 11:53:43 -05:00
|
|
|
match self.log_view_grouping {
|
|
|
|
|
LogViewGrouping::ByDate => self.format_by_date(),
|
|
|
|
|
LogViewGrouping::ByProject => self.format_by_project(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn format_by_date(&mut self) {
|
|
|
|
|
use chrono::{DateTime, Local, Timelike};
|
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
|
|
2025-11-22 11:27:01 -05:00
|
|
|
// Group frames by date
|
|
|
|
|
let mut by_date: BTreeMap<String, Vec<&WatsonFrame>> = BTreeMap::new();
|
|
|
|
|
|
|
|
|
|
for frame in &self.log_view_frames {
|
|
|
|
|
if let Ok(start_dt) = DateTime::parse_from_rfc3339(&frame.start) {
|
|
|
|
|
let local_dt: DateTime<Local> = start_dt.into();
|
|
|
|
|
let date_key = local_dt.format("%A %d %B %Y").to_string();
|
|
|
|
|
by_date.entry(date_key).or_insert_with(Vec::new).push(frame);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut lines = Vec::new();
|
|
|
|
|
|
|
|
|
|
for (date, frames) in by_date.iter().rev() {
|
|
|
|
|
lines.push(date.clone());
|
|
|
|
|
|
|
|
|
|
for frame in frames {
|
|
|
|
|
if let (Ok(start_dt), Ok(stop_dt)) = (
|
|
|
|
|
DateTime::parse_from_rfc3339(&frame.start),
|
|
|
|
|
DateTime::parse_from_rfc3339(&frame.stop),
|
|
|
|
|
) {
|
|
|
|
|
let start_local: DateTime<Local> = start_dt.into();
|
|
|
|
|
let stop_local: DateTime<Local> = stop_dt.into();
|
|
|
|
|
|
|
|
|
|
let start_time = format!("{:02}:{:02}", start_local.hour(), start_local.minute());
|
|
|
|
|
let stop_time = format!("{:02}:{:02}", stop_local.hour(), stop_local.minute());
|
|
|
|
|
|
|
|
|
|
let tags_str = if frame.tags.is_empty() {
|
|
|
|
|
String::new()
|
|
|
|
|
} else {
|
|
|
|
|
format!(" [{}]", frame.tags.join(", "))
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
lines.push(format!(
|
|
|
|
|
"\t{} to {} {}{}",
|
|
|
|
|
start_time, stop_time, frame.project, tags_str
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
lines.push(String::new()); // Empty line between dates
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.log_view_content = lines;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-22 11:53:43 -05:00
|
|
|
fn format_by_project(&mut self) {
|
|
|
|
|
use chrono::{DateTime, Local, Timelike};
|
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
|
|
|
|
|
|
// Group frames by date, then by project within each date
|
|
|
|
|
let mut by_date: BTreeMap<String, BTreeMap<String, Vec<&WatsonFrame>>> = BTreeMap::new();
|
|
|
|
|
|
|
|
|
|
for frame in &self.log_view_frames {
|
|
|
|
|
if let Ok(start_dt) = DateTime::parse_from_rfc3339(&frame.start) {
|
|
|
|
|
let local_dt: DateTime<Local> = start_dt.into();
|
|
|
|
|
let date_key = local_dt.format("%A %d %B %Y").to_string();
|
|
|
|
|
by_date
|
|
|
|
|
.entry(date_key)
|
|
|
|
|
.or_insert_with(BTreeMap::new)
|
|
|
|
|
.entry(frame.project.clone())
|
|
|
|
|
.or_insert_with(Vec::new)
|
|
|
|
|
.push(frame);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut lines = Vec::new();
|
|
|
|
|
|
|
|
|
|
for (date, projects) in by_date.iter().rev() {
|
|
|
|
|
lines.push(date.clone());
|
|
|
|
|
|
|
|
|
|
for (project, frames) in projects.iter() {
|
|
|
|
|
lines.push(format!(" {}", project)); // Project header with indent
|
|
|
|
|
|
|
|
|
|
for frame in frames {
|
|
|
|
|
if let (Ok(start_dt), Ok(stop_dt)) = (
|
|
|
|
|
DateTime::parse_from_rfc3339(&frame.start),
|
|
|
|
|
DateTime::parse_from_rfc3339(&frame.stop),
|
|
|
|
|
) {
|
|
|
|
|
let start_local: DateTime<Local> = start_dt.into();
|
|
|
|
|
let stop_local: DateTime<Local> = stop_dt.into();
|
|
|
|
|
|
|
|
|
|
let start_time = format!("{:02}:{:02}", start_local.hour(), start_local.minute());
|
|
|
|
|
let stop_time = format!("{:02}:{:02}", stop_local.hour(), stop_local.minute());
|
|
|
|
|
|
|
|
|
|
let tags_str = if frame.tags.is_empty() {
|
|
|
|
|
String::new()
|
|
|
|
|
} else {
|
|
|
|
|
format!(" [{}]", frame.tags.join(", "))
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
lines.push(format!(
|
|
|
|
|
"\t\t{} to {}{}",
|
|
|
|
|
start_time, stop_time, tags_str
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
lines.push(String::new()); // Empty line between dates
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.log_view_content = lines;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-22 11:27:01 -05:00
|
|
|
fn edit_selected_frame(&mut self) -> anyhow::Result<()> {
|
|
|
|
|
if self.log_view_frames.is_empty() || self.log_view_selected >= self.log_view_frames.len() {
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let frame_id = &self.log_view_frames[self.log_view_selected].id;
|
|
|
|
|
|
|
|
|
|
use crossterm::{
|
|
|
|
|
execute,
|
|
|
|
|
terminal::{disable_raw_mode, enable_raw_mode},
|
|
|
|
|
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
|
|
|
|
|
};
|
|
|
|
|
use std::io::stdout;
|
|
|
|
|
|
|
|
|
|
// Leave TUI mode
|
|
|
|
|
disable_raw_mode()?;
|
|
|
|
|
execute!(stdout(), LeaveAlternateScreen)?;
|
|
|
|
|
|
|
|
|
|
// Run watson edit
|
|
|
|
|
let status = Command::new("watson").arg("edit").arg(frame_id).status()?;
|
|
|
|
|
|
|
|
|
|
// Return to TUI mode
|
|
|
|
|
execute!(stdout(), EnterAlternateScreen)?;
|
|
|
|
|
enable_raw_mode()?;
|
|
|
|
|
|
|
|
|
|
if status.success() {
|
|
|
|
|
// Reload log content
|
|
|
|
|
self.load_log_content()?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.needs_clear = true;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn delete_selected_frame(&mut self) -> anyhow::Result<()> {
|
|
|
|
|
if self.log_view_frames.is_empty() || self.log_view_selected >= self.log_view_frames.len() {
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let frame_id = self.log_view_frames[self.log_view_selected].id.clone();
|
|
|
|
|
|
|
|
|
|
// Run watson remove with --force flag (no confirmation)
|
|
|
|
|
let output = Command::new("watson")
|
|
|
|
|
.arg("remove")
|
|
|
|
|
.arg("--force")
|
|
|
|
|
.arg(&frame_id)
|
|
|
|
|
.output()?;
|
|
|
|
|
|
|
|
|
|
if output.status.success() {
|
|
|
|
|
// Reload log content
|
|
|
|
|
self.load_log_content()?;
|
|
|
|
|
|
|
|
|
|
// Adjust selection if we deleted the last item
|
|
|
|
|
if self.log_view_selected >= self.log_view_frames.len() && !self.log_view_frames.is_empty() {
|
|
|
|
|
self.log_view_selected = self.log_view_frames.len() - 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.needs_clear = true;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-22 11:35:49 -05:00
|
|
|
fn copy_log_to_clipboard(&mut self) -> anyhow::Result<()> {
|
|
|
|
|
if self.log_view_content.is_empty() {
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-23 12:01:13 -05:00
|
|
|
// Determine what to copy based on selection level
|
|
|
|
|
let text = match self.log_view_selection_level {
|
|
|
|
|
LogViewSelection::Entry => {
|
|
|
|
|
// Copy just the selected entry
|
|
|
|
|
self.get_selected_entry_text()
|
|
|
|
|
}
|
|
|
|
|
LogViewSelection::Project => {
|
|
|
|
|
// Copy the selected project group
|
|
|
|
|
self.get_selected_project_text()
|
|
|
|
|
}
|
|
|
|
|
LogViewSelection::Day => {
|
|
|
|
|
// Copy the entire day
|
|
|
|
|
self.get_selected_day_text()
|
|
|
|
|
}
|
2025-11-23 12:05:10 -05:00
|
|
|
LogViewSelection::All => {
|
|
|
|
|
// Copy the entire view/period
|
|
|
|
|
self.log_view_content.join("\n")
|
|
|
|
|
}
|
2025-11-23 12:01:13 -05:00
|
|
|
};
|
2025-11-22 11:35:49 -05:00
|
|
|
|
|
|
|
|
// Copy to clipboard using the persistent clipboard instance
|
|
|
|
|
if let Some(ref mut clipboard) = self.clipboard {
|
|
|
|
|
if let Err(e) = clipboard.set_text(&text) {
|
|
|
|
|
self.set_status_message(format!("Failed to copy: {}", e));
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Try to create a new clipboard if we don't have one
|
|
|
|
|
match arboard::Clipboard::new() {
|
|
|
|
|
Ok(mut clipboard) => {
|
|
|
|
|
if let Err(e) = clipboard.set_text(&text) {
|
|
|
|
|
self.set_status_message(format!("Failed to copy: {}", e));
|
|
|
|
|
}
|
|
|
|
|
self.clipboard = Some(clipboard);
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
self.set_status_message(format!("Failed to access clipboard: {}", e));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-23 12:01:13 -05:00
|
|
|
fn get_selected_entry_text(&self) -> String {
|
|
|
|
|
// Find the entry line corresponding to the selected frame
|
|
|
|
|
let entry_lines: Vec<&String> = self
|
|
|
|
|
.log_view_content
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|l| self.is_entry_line(l))
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
if self.log_view_selected < entry_lines.len() {
|
|
|
|
|
entry_lines[self.log_view_selected].to_string()
|
|
|
|
|
} else {
|
|
|
|
|
String::new()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_selected_project_text(&self) -> String {
|
|
|
|
|
// Find which project group the selected entry belongs to
|
|
|
|
|
let mut current_project_lines = Vec::new();
|
|
|
|
|
let mut frame_count = 0;
|
|
|
|
|
let mut found = false;
|
|
|
|
|
|
|
|
|
|
for line in &self.log_view_content {
|
|
|
|
|
if line.starts_with(" ") && !line.starts_with("\t") {
|
|
|
|
|
// This is a project header
|
|
|
|
|
if found {
|
|
|
|
|
break; // We've collected the target project
|
|
|
|
|
}
|
|
|
|
|
current_project_lines.clear();
|
|
|
|
|
current_project_lines.push(line.clone());
|
|
|
|
|
} else if self.is_entry_line(line) {
|
|
|
|
|
// Entry within a project
|
|
|
|
|
current_project_lines.push(line.clone());
|
|
|
|
|
if frame_count == self.log_view_selected {
|
|
|
|
|
found = true;
|
|
|
|
|
}
|
|
|
|
|
frame_count += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
current_project_lines.join("\n")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_selected_day_text(&self) -> String {
|
|
|
|
|
// Find which day the selected entry belongs to
|
|
|
|
|
let mut current_day_lines = Vec::new();
|
|
|
|
|
let mut frame_count = 0;
|
|
|
|
|
let mut found = false;
|
|
|
|
|
|
|
|
|
|
for line in &self.log_view_content {
|
|
|
|
|
if !line.starts_with('\t') && !line.is_empty() && !line.starts_with(" ") {
|
|
|
|
|
// This is a date header
|
|
|
|
|
if found {
|
|
|
|
|
break; // We've collected the target day
|
|
|
|
|
}
|
|
|
|
|
current_day_lines.clear();
|
|
|
|
|
current_day_lines.push(line.clone());
|
|
|
|
|
} else if line.starts_with('\t') || line.starts_with(" ") {
|
|
|
|
|
// Add all lines within the day (project headers and entries)
|
|
|
|
|
current_day_lines.push(line.clone());
|
|
|
|
|
|
|
|
|
|
// Count only actual entries
|
|
|
|
|
if self.is_entry_line(line) {
|
|
|
|
|
if frame_count == self.log_view_selected {
|
|
|
|
|
found = true;
|
|
|
|
|
}
|
|
|
|
|
frame_count += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
current_day_lines.join("\n")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn jump_to_next_project(&mut self) {
|
|
|
|
|
// Find the current project, then jump to first entry of next project
|
|
|
|
|
let mut current_date = String::new();
|
|
|
|
|
let mut current_project = String::new();
|
|
|
|
|
let mut selected_project = String::new();
|
|
|
|
|
let mut selected_date = String::new();
|
|
|
|
|
let mut frame_count = 0;
|
|
|
|
|
let mut found_current = false;
|
|
|
|
|
|
|
|
|
|
// Find which project we're currently in
|
|
|
|
|
for line in &self.log_view_content.clone() {
|
|
|
|
|
if !line.starts_with('\t') && !line.is_empty() && !line.starts_with(" ") {
|
|
|
|
|
current_date = line.clone();
|
|
|
|
|
} else if line.starts_with(" ") && !line.starts_with("\t") {
|
|
|
|
|
current_project = line.clone();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.is_entry_line(line) {
|
|
|
|
|
if frame_count == self.log_view_selected {
|
|
|
|
|
selected_project = current_project.clone();
|
|
|
|
|
selected_date = current_date.clone();
|
|
|
|
|
found_current = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
frame_count += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !found_current {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Now find the next project within the same day first, then next day
|
|
|
|
|
current_project = String::new();
|
|
|
|
|
current_date = String::new();
|
|
|
|
|
let mut past_selected = false;
|
|
|
|
|
frame_count = 0;
|
|
|
|
|
|
|
|
|
|
for line in &self.log_view_content.clone() {
|
|
|
|
|
if !line.starts_with('\t') && !line.is_empty() && !line.starts_with(" ") {
|
|
|
|
|
current_date = line.clone();
|
|
|
|
|
} else if line.starts_with(" ") && !line.starts_with("\t") {
|
|
|
|
|
current_project = line.clone();
|
|
|
|
|
if past_selected && current_project != selected_project {
|
|
|
|
|
// This is the next project, select its first entry
|
|
|
|
|
self.log_view_selected = frame_count;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.is_entry_line(line) {
|
|
|
|
|
if current_project == selected_project && current_date == selected_date {
|
|
|
|
|
past_selected = true;
|
|
|
|
|
}
|
|
|
|
|
frame_count += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn jump_to_previous_project(&mut self) {
|
|
|
|
|
// Find the previous project and jump to its first entry
|
|
|
|
|
let mut current_project = String::new();
|
2025-11-23 12:11:46 -05:00
|
|
|
let mut _selected_project = String::new();
|
2025-11-23 12:01:13 -05:00
|
|
|
let mut frame_count = 0;
|
|
|
|
|
let mut previous_project_first_entry = None;
|
|
|
|
|
let mut last_different_project_entry = None;
|
|
|
|
|
|
|
|
|
|
// Find which project we're currently in
|
|
|
|
|
for line in &self.log_view_content.clone() {
|
2025-11-23 12:11:46 -05:00
|
|
|
if line.starts_with(" ") && !line.starts_with("\t") {
|
|
|
|
|
if !current_project.is_empty() && current_project != _selected_project {
|
2025-11-23 12:01:13 -05:00
|
|
|
last_different_project_entry = previous_project_first_entry;
|
|
|
|
|
}
|
|
|
|
|
current_project = line.clone();
|
|
|
|
|
previous_project_first_entry = Some(frame_count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.is_entry_line(line) {
|
|
|
|
|
if frame_count == self.log_view_selected {
|
2025-11-23 12:11:46 -05:00
|
|
|
_selected_project = current_project.clone();
|
2025-11-23 12:01:13 -05:00
|
|
|
// Jump to the last different project we saw
|
|
|
|
|
if let Some(entry) = last_different_project_entry {
|
|
|
|
|
self.log_view_selected = entry;
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
frame_count += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn jump_to_next_day(&mut self) {
|
|
|
|
|
// Find the current day, then jump to first entry of next day
|
|
|
|
|
let mut current_date = String::new();
|
|
|
|
|
let mut selected_date = String::new();
|
|
|
|
|
let mut frame_count = 0;
|
|
|
|
|
let mut found_current = false;
|
|
|
|
|
|
|
|
|
|
// Find which day we're currently in
|
|
|
|
|
for line in &self.log_view_content.clone() {
|
|
|
|
|
if !line.starts_with('\t') && !line.is_empty() && !line.starts_with(" ") {
|
|
|
|
|
current_date = line.clone();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.is_entry_line(line) {
|
|
|
|
|
if frame_count == self.log_view_selected {
|
|
|
|
|
selected_date = current_date.clone();
|
|
|
|
|
found_current = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
frame_count += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !found_current {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Now find the next day and jump to its first entry
|
|
|
|
|
current_date = String::new();
|
|
|
|
|
let mut past_selected = false;
|
|
|
|
|
frame_count = 0;
|
|
|
|
|
|
|
|
|
|
for line in &self.log_view_content.clone() {
|
|
|
|
|
if !line.starts_with('\t') && !line.is_empty() && !line.starts_with(" ") {
|
|
|
|
|
if past_selected && line != &selected_date {
|
|
|
|
|
// This is the next day, continue to find its first entry
|
|
|
|
|
current_date = line.clone();
|
|
|
|
|
} else if line == &selected_date {
|
|
|
|
|
past_selected = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.is_entry_line(line) {
|
|
|
|
|
if past_selected && !current_date.is_empty() && current_date != selected_date {
|
|
|
|
|
// First entry of next day
|
|
|
|
|
self.log_view_selected = frame_count;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
frame_count += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn jump_to_previous_day(&mut self) {
|
|
|
|
|
// Find the previous day and jump to its first entry
|
|
|
|
|
let mut current_date = String::new();
|
2025-11-23 12:11:46 -05:00
|
|
|
let mut _selected_date = String::new();
|
2025-11-23 12:01:13 -05:00
|
|
|
let mut frame_count = 0;
|
|
|
|
|
let mut last_different_day_entry = None;
|
|
|
|
|
let mut previous_day_first_entry = None;
|
|
|
|
|
|
|
|
|
|
// Find which day we're currently in
|
|
|
|
|
for line in &self.log_view_content.clone() {
|
|
|
|
|
if !line.starts_with('\t') && !line.is_empty() && !line.starts_with(" ") {
|
2025-11-23 12:11:46 -05:00
|
|
|
if !current_date.is_empty() && current_date != _selected_date {
|
2025-11-23 12:01:13 -05:00
|
|
|
last_different_day_entry = previous_day_first_entry;
|
|
|
|
|
}
|
|
|
|
|
current_date = line.clone();
|
|
|
|
|
previous_day_first_entry = Some(frame_count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.is_entry_line(line) {
|
|
|
|
|
if frame_count == self.log_view_selected {
|
2025-11-23 12:11:46 -05:00
|
|
|
_selected_date = current_date.clone();
|
2025-11-23 12:01:13 -05:00
|
|
|
// Jump to the last different day we saw
|
|
|
|
|
if let Some(entry) = last_different_day_entry {
|
|
|
|
|
self.log_view_selected = entry;
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
frame_count += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-16 09:57:01 -05:00
|
|
|
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() {
|
2025-11-16 13:12:45 -05:00
|
|
|
if self
|
|
|
|
|
.state
|
|
|
|
|
.active_timer
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|(active, _)| active.name == item.name)
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
{
|
2025-11-16 09:57:01 -05:00
|
|
|
self.state.stop_timer()?;
|
|
|
|
|
} else {
|
|
|
|
|
self.state.start_timer(item)?;
|
|
|
|
|
}
|
|
|
|
|
self.state.save()?;
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-16 12:25:22 -05:00
|
|
|
fn delete_current_item(&mut self) -> anyhow::Result<()> {
|
|
|
|
|
// Check if this is the active timer
|
|
|
|
|
let should_stop = {
|
|
|
|
|
let items = match self.state.current_pane {
|
|
|
|
|
0 => &self.state.permanent_items,
|
|
|
|
|
1 => &self.state.recurring_items,
|
|
|
|
|
2 => &self.state.recent_items,
|
|
|
|
|
_ => return Ok(()),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let index = self.state.selected_indices[self.state.current_pane];
|
2025-11-16 13:12:45 -05:00
|
|
|
|
2025-11-16 12:25:22 -05:00
|
|
|
if !items.is_empty() && index < items.len() {
|
|
|
|
|
if let Some((ref active, _)) = self.state.active_timer {
|
|
|
|
|
items[index].name == active.name
|
|
|
|
|
} else {
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Stop timer if needed
|
|
|
|
|
if should_stop {
|
|
|
|
|
self.state.stop_timer()?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Now delete the item
|
|
|
|
|
let items = match self.state.current_pane {
|
|
|
|
|
0 => &mut self.state.permanent_items,
|
|
|
|
|
1 => &mut self.state.recurring_items,
|
|
|
|
|
2 => &mut self.state.recent_items,
|
|
|
|
|
_ => return Ok(()),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let index = self.state.selected_indices[self.state.current_pane];
|
2025-11-16 13:12:45 -05:00
|
|
|
|
2025-11-16 12:25:22 -05:00
|
|
|
if !items.is_empty() && index < items.len() {
|
|
|
|
|
// Remove the item
|
|
|
|
|
items.remove(index);
|
2025-11-16 13:12:45 -05:00
|
|
|
|
2025-11-16 12:25:22 -05:00
|
|
|
// Adjust index if we're at the end
|
|
|
|
|
if !items.is_empty() && index == items.len() {
|
|
|
|
|
self.state.selected_indices[self.state.current_pane] = items.len() - 1;
|
|
|
|
|
}
|
2025-11-16 13:12:45 -05:00
|
|
|
|
2025-11-16 12:25:22 -05:00
|
|
|
// Save changes
|
|
|
|
|
self.state.save()?;
|
|
|
|
|
}
|
2025-11-16 13:12:45 -05:00
|
|
|
|
2025-11-16 12:25:22 -05:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn set_status_message<S: Into<String>>(&mut self, message: S) {
|
|
|
|
|
self.status_message = Some((message.into(), std::time::Instant::now()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn update_status_message(&mut self) {
|
|
|
|
|
// Clear status message after 3 seconds
|
|
|
|
|
if let Some((_, instant)) = self.status_message {
|
|
|
|
|
if instant.elapsed().as_secs() >= 3 {
|
|
|
|
|
self.status_message = None;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-22 10:45:10 -05:00
|
|
|
fn start_reassign_project(&mut self) {
|
|
|
|
|
if let Some(item) = self.get_current_item() {
|
|
|
|
|
self.current_screen = Screen::ReassignProject;
|
|
|
|
|
// Pre-fill with current project if it exists
|
|
|
|
|
self.reassign_project_buffer = item.tags.first().cloned().unwrap_or_default();
|
|
|
|
|
self.reassign_project_cursor = self.reassign_project_buffer.len();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-22 11:12:46 -05:00
|
|
|
fn start_log_view(&mut self) -> anyhow::Result<()> {
|
|
|
|
|
self.current_screen = Screen::LogView;
|
|
|
|
|
self.log_view_period = LogViewPeriod::Day;
|
|
|
|
|
self.log_view_scroll = 0;
|
|
|
|
|
self.load_log_content()?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn load_log_content(&mut self) -> anyhow::Result<()> {
|
|
|
|
|
let flag = match self.log_view_period {
|
|
|
|
|
LogViewPeriod::Day => "--day",
|
|
|
|
|
LogViewPeriod::Week => "--week",
|
|
|
|
|
LogViewPeriod::Month => "--month",
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-22 11:27:01 -05:00
|
|
|
let output = Command::new("watson")
|
|
|
|
|
.arg("log")
|
|
|
|
|
.arg(flag)
|
|
|
|
|
.arg("--json")
|
|
|
|
|
.output()?;
|
2025-11-22 11:12:46 -05:00
|
|
|
|
|
|
|
|
if output.status.success() {
|
2025-11-22 11:27:01 -05:00
|
|
|
let json_str = String::from_utf8_lossy(&output.stdout);
|
|
|
|
|
|
|
|
|
|
// Parse JSON frames
|
|
|
|
|
match serde_json::from_str::<Vec<WatsonFrame>>(&json_str) {
|
|
|
|
|
Ok(frames) => {
|
|
|
|
|
self.log_view_frames = frames;
|
|
|
|
|
self.format_log_entries();
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
self.log_view_frames.clear();
|
|
|
|
|
self.log_view_content = vec![
|
|
|
|
|
"Failed to parse Watson log JSON:".to_string(),
|
|
|
|
|
e.to_string(),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-22 11:12:46 -05:00
|
|
|
} else {
|
|
|
|
|
let error = String::from_utf8_lossy(&output.stderr);
|
2025-11-22 11:27:01 -05:00
|
|
|
self.log_view_frames.clear();
|
2025-11-22 11:12:46 -05:00
|
|
|
self.log_view_content = vec![
|
|
|
|
|
"Failed to load Watson log:".to_string(),
|
|
|
|
|
error.to_string(),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-22 10:45:10 -05:00
|
|
|
fn reassign_project_for_current_item(&mut self) -> anyhow::Result<()> {
|
|
|
|
|
let items = match self.state.current_pane {
|
|
|
|
|
0 => &mut self.state.permanent_items,
|
|
|
|
|
1 => &mut self.state.recurring_items,
|
|
|
|
|
2 => &mut self.state.recent_items,
|
|
|
|
|
_ => return Ok(()),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let index = self.state.selected_indices[self.state.current_pane];
|
|
|
|
|
|
|
|
|
|
let needs_restart = if let Some(item) = items.get_mut(index) {
|
|
|
|
|
// Update the tags
|
|
|
|
|
if self.reassign_project_buffer.is_empty() {
|
|
|
|
|
item.tags.clear();
|
|
|
|
|
} else {
|
|
|
|
|
item.tags = vec![self.reassign_project_buffer.clone()];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if this is the active timer
|
|
|
|
|
self.state
|
|
|
|
|
.active_timer
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|(active, _)| active.name == item.name)
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
} else {
|
|
|
|
|
false
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// If this was the active timer, restart it with new tags
|
|
|
|
|
if needs_restart {
|
|
|
|
|
let item = items[index].clone();
|
|
|
|
|
self.state.stop_timer()?;
|
|
|
|
|
self.state.start_timer(item)?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.state.save()?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-16 10:19:14 -05:00
|
|
|
fn edit_app_config(&mut self) -> anyhow::Result<()> {
|
|
|
|
|
use crossterm::{
|
|
|
|
|
execute,
|
2025-11-16 13:12:45 -05:00
|
|
|
terminal::{disable_raw_mode, enable_raw_mode},
|
|
|
|
|
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
|
2025-11-16 10:19:14 -05:00
|
|
|
};
|
|
|
|
|
use std::io::stdout;
|
2025-11-16 13:12:45 -05:00
|
|
|
|
2025-11-16 10:19:14 -05:00
|
|
|
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string());
|
|
|
|
|
let config_path = Config::config_path()?;
|
2025-11-16 13:12:45 -05:00
|
|
|
|
2025-11-16 10:19:14 -05:00
|
|
|
// Leave TUI mode
|
|
|
|
|
disable_raw_mode()?;
|
|
|
|
|
execute!(stdout(), LeaveAlternateScreen)?;
|
|
|
|
|
|
|
|
|
|
// Run editor
|
2025-11-16 13:12:45 -05:00
|
|
|
let status = Command::new(editor).arg(&config_path).status()?;
|
2025-11-16 10:19:14 -05:00
|
|
|
|
|
|
|
|
if status.success() {
|
|
|
|
|
// Reload entire application state
|
|
|
|
|
self.config = Config::load()?;
|
|
|
|
|
self.current_screen = Screen::Main;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return to TUI mode
|
|
|
|
|
execute!(stdout(), EnterAlternateScreen)?;
|
|
|
|
|
enable_raw_mode()?;
|
2025-11-22 11:12:46 -05:00
|
|
|
|
|
|
|
|
self.needs_clear = true;
|
2025-11-16 13:12:45 -05:00
|
|
|
|
2025-11-16 10:19:14 -05:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn edit_config(&mut self) -> anyhow::Result<()> {
|
|
|
|
|
use crossterm::{
|
|
|
|
|
execute,
|
2025-11-16 13:12:45 -05:00
|
|
|
terminal::{disable_raw_mode, enable_raw_mode},
|
|
|
|
|
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
|
2025-11-16 10:19:14 -05:00
|
|
|
};
|
|
|
|
|
use std::io::stdout;
|
2025-11-16 13:12:45 -05:00
|
|
|
|
2025-11-16 09:57:01 -05:00
|
|
|
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string());
|
|
|
|
|
let config_path = AppState::config_file()?;
|
2025-11-16 13:12:45 -05:00
|
|
|
|
2025-11-16 09:57:01 -05:00
|
|
|
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")?;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-16 10:19:14 -05:00
|
|
|
// Leave TUI mode
|
|
|
|
|
disable_raw_mode()?;
|
|
|
|
|
execute!(stdout(), LeaveAlternateScreen)?;
|
|
|
|
|
|
|
|
|
|
// Run editor
|
2025-11-16 13:12:45 -05:00
|
|
|
let status = Command::new(editor).arg(&config_path).status()?;
|
2025-11-16 10:19:14 -05:00
|
|
|
|
|
|
|
|
if status.success() {
|
|
|
|
|
// Reload entire application state
|
|
|
|
|
self.state = AppState::load()?;
|
|
|
|
|
self.current_screen = Screen::Main;
|
2025-11-16 09:57:01 -05:00
|
|
|
}
|
|
|
|
|
|
2025-11-16 10:19:14 -05:00
|
|
|
// Return to TUI mode
|
|
|
|
|
execute!(stdout(), EnterAlternateScreen)?;
|
|
|
|
|
enable_raw_mode()?;
|
2025-11-22 11:12:46 -05:00
|
|
|
|
|
|
|
|
self.needs_clear = true;
|
2025-11-16 13:12:45 -05:00
|
|
|
|
2025-11-16 09:57:01 -05:00
|
|
|
Ok(())
|
|
|
|
|
}
|
2025-11-16 13:12:45 -05:00
|
|
|
}
|