diff --git a/src/app.rs b/src/app.rs index bab4b65..9bb4581 100644 --- a/src/app.rs +++ b/src/app.rs @@ -22,6 +22,7 @@ pub enum Screen { ReassignProject, LogView, LogViewHelp, + AddAnnotation, } pub enum LogViewPeriod { @@ -70,6 +71,8 @@ pub struct App { pub help_scroll: usize, pub clipboard: Option, pub status_message: Option<(String, std::time::Instant)>, + pub annotation_buffer: String, + pub annotation_cursor: usize, } pub enum NewEntryMode { @@ -130,6 +133,8 @@ impl App { help_scroll: 0, clipboard: arboard::Clipboard::new().ok(), status_message: None, + annotation_buffer: String::new(), + annotation_cursor: 0, }) } @@ -147,6 +152,7 @@ impl App { Screen::ReassignProject => self.handle_reassign_project_event(event), Screen::LogView => self.handle_log_view_event(event), Screen::LogViewHelp => self.handle_log_view_help_event(event), + Screen::AddAnnotation => self.handle_add_annotation_event(event), }; // If we switched screens, signal that we need to clear @@ -487,6 +493,12 @@ impl App { self.edit_selected_frame()?; } } + KeyCode::Char('a') => { + // Only allow annotation when selecting individual entry + if matches!(self.log_view_selection_level, LogViewSelection::Entry) { + self.start_add_annotation(); + } + } KeyCode::Char('x') => { // Only allow delete when selecting individual entry if matches!(self.log_view_selection_level, LogViewSelection::Entry) { @@ -1658,4 +1670,115 @@ impl App { Ok(()) } + + fn start_add_annotation(&mut self) { + self.current_screen = Screen::AddAnnotation; + self.annotation_buffer.clear(); + self.annotation_cursor = 0; + } + + fn handle_add_annotation_event(&mut self, event: Event) -> anyhow::Result { + match event { + Event::Key(KeyEvent { + code, modifiers, .. + }) => { + match (code, modifiers) { + (KeyCode::Esc, _) => { + self.current_screen = Screen::LogView; + self.annotation_buffer.clear(); + self.annotation_cursor = 0; + } + (KeyCode::Enter, _) => { + if !self.annotation_buffer.is_empty() { + self.add_annotation_to_frame()?; + } + self.current_screen = Screen::LogView; + self.annotation_buffer.clear(); + self.annotation_cursor = 0; + } + (KeyCode::Backspace, _) => { + if self.annotation_cursor > 0 { + let idx = self.annotation_cursor - 1; + if idx < self.annotation_buffer.len() { + self.annotation_buffer.remove(idx); + self.annotation_cursor -= 1; + } + } + } + (KeyCode::Char(c), m) if m.is_empty() => { + if self.annotation_cursor <= self.annotation_buffer.len() { + self.annotation_buffer.insert(self.annotation_cursor, c); + self.annotation_cursor += 1; + } + } + _ => {} + } + } + _ => {} + } + Ok(false) + } + + fn add_annotation_to_frame(&mut self) -> anyhow::Result<()> { + use serde_json::Value; + + // Check if selection is valid + if self.log_view_selected >= self.log_view_frame_indices.len() { + self.set_status_message("Invalid selection"); + return Ok(()); + } + + // Get the actual frame index from the display index + let frame_idx = self.log_view_frame_indices[self.log_view_selected]; + if frame_idx >= self.log_view_frames.len() { + return Ok(()); + } + + let frame_id = self.log_view_frames[frame_idx].id.clone(); + + // Read watson frames file + let frames_path = dirs::config_dir() + .ok_or_else(|| anyhow::anyhow!("Could not find config directory"))? + .join("watson") + .join("frames"); + + let frames_content = std::fs::read_to_string(&frames_path)?; + let mut frames: Value = serde_json::from_str(&frames_content)?; + + // Find and update the frame with matching id + if let Some(frames_array) = frames.as_array_mut() { + for frame in frames_array { + if let Some(frame_array) = frame.as_array_mut() { + // Frame format: [start, stop, project, id, tags, updated] + if frame_array.len() > 4 { + if let Some(id) = frame_array[3].as_str() { + if id == frame_id { + // Update tags array (index 4) + if let Some(tags) = frame_array[4].as_array_mut() { + // Add the annotation tag + tags.push(Value::String(self.annotation_buffer.clone())); + } + // Update timestamp (index 5) + if frame_array.len() > 5 { + frame_array[5] = Value::Number(chrono::Utc::now().timestamp().into()); + } + break; + } + } + } + } + } + } + + // Write back to frames file + let updated_content = serde_json::to_string_pretty(&frames)?; + std::fs::write(&frames_path, updated_content)?; + + // Reload log content + self.load_log_content()?; + self.set_status_message("Annotation added"); + + self.needs_clear = true; + Ok(()) + } } diff --git a/src/ui.rs b/src/ui.rs index d2a1a1f..9dcab36 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -24,6 +24,7 @@ pub fn render(frame: &mut Frame, app: &App) { Screen::ReassignProject => render_reassign_project(frame, app), Screen::LogView => render_log_view(frame, app), Screen::LogViewHelp => render_log_view_help(frame, app), + Screen::AddAnnotation => render_add_annotation(frame, app), } } @@ -957,6 +958,7 @@ fn render_log_view_help(frame: &mut Frame, app: &App) { "", "Actions:", "- e: Edit the selected entry (Entry level only)", + "- a: Add annotation tag to the selected entry (Entry level only)", "- x: Delete the selected entry (Entry level only)", "- b: Backfill - set entry start time to previous entry's end time", " (Entry level, By Date view only)", @@ -1005,3 +1007,51 @@ fn render_log_view_help(frame: &mut Frame, app: &App) { frame.render_widget(command_bar, bar_area); } + +fn render_add_annotation(frame: &mut Frame, app: &App) { + let area = centered_rect(60, 5, frame.size()); + frame.render_widget(Clear, area); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(3), Constraint::Length(2)]) + .split(area); + + // Get current entry to show in title + let current_entry = if app.log_view_selected < app.log_view_frame_indices.len() { + let frame_idx = app.log_view_frame_indices[app.log_view_selected]; + app.log_view_frames.get(frame_idx).map(|frame| { + if frame.tags.is_empty() { + frame.project.clone() + } else { + format!("{} [{}]", frame.tags.join(", "), frame.project) + } + }) + } else { + None + }; + + let title = if let Some(entry) = current_entry { + format!("Add Annotation to: {}", entry) + } else { + "Add Annotation".to_string() + }; + + // Annotation input + let annotation_block = Block::default() + .title(title) + .borders(Borders::ALL) + .style(Style::default().fg(ACTIVE_COLOR)); + + let annotation_text = Paragraph::new(app.annotation_buffer.as_str()) + .block(annotation_block); + + frame.render_widget(annotation_text, chunks[0]); + + // Help text + let help_text = Paragraph::new("Enter annotation text, press Enter to save, Esc to cancel") + .style(Style::default().fg(Color::DarkGray)) + .alignment(Alignment::Center); + + frame.render_widget(help_text, chunks[1]); +}