Add annotations

This commit is contained in:
Ian Keane 2025-11-25 20:25:29 -05:00
parent 89d1c02690
commit 599cc22463
2 changed files with 173 additions and 0 deletions

View file

@ -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<arboard::Clipboard>,
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<bool> {
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(())
}
}

View file

@ -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]);
}