Add annotations
This commit is contained in:
parent
89d1c02690
commit
599cc22463
2 changed files with 173 additions and 0 deletions
123
src/app.rs
123
src/app.rs
|
|
@ -22,6 +22,7 @@ pub enum Screen {
|
||||||
ReassignProject,
|
ReassignProject,
|
||||||
LogView,
|
LogView,
|
||||||
LogViewHelp,
|
LogViewHelp,
|
||||||
|
AddAnnotation,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum LogViewPeriod {
|
pub enum LogViewPeriod {
|
||||||
|
|
@ -70,6 +71,8 @@ pub struct App {
|
||||||
pub help_scroll: usize,
|
pub help_scroll: usize,
|
||||||
pub clipboard: Option<arboard::Clipboard>,
|
pub clipboard: Option<arboard::Clipboard>,
|
||||||
pub status_message: Option<(String, std::time::Instant)>,
|
pub status_message: Option<(String, std::time::Instant)>,
|
||||||
|
pub annotation_buffer: String,
|
||||||
|
pub annotation_cursor: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum NewEntryMode {
|
pub enum NewEntryMode {
|
||||||
|
|
@ -130,6 +133,8 @@ impl App {
|
||||||
help_scroll: 0,
|
help_scroll: 0,
|
||||||
clipboard: arboard::Clipboard::new().ok(),
|
clipboard: arboard::Clipboard::new().ok(),
|
||||||
status_message: None,
|
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::ReassignProject => self.handle_reassign_project_event(event),
|
||||||
Screen::LogView => self.handle_log_view_event(event),
|
Screen::LogView => self.handle_log_view_event(event),
|
||||||
Screen::LogViewHelp => self.handle_log_view_help_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
|
// If we switched screens, signal that we need to clear
|
||||||
|
|
@ -487,6 +493,12 @@ impl App {
|
||||||
self.edit_selected_frame()?;
|
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') => {
|
KeyCode::Char('x') => {
|
||||||
// Only allow delete when selecting individual entry
|
// Only allow delete when selecting individual entry
|
||||||
if matches!(self.log_view_selection_level, LogViewSelection::Entry) {
|
if matches!(self.log_view_selection_level, LogViewSelection::Entry) {
|
||||||
|
|
@ -1658,4 +1670,115 @@ impl App {
|
||||||
|
|
||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
50
src/ui.rs
50
src/ui.rs
|
|
@ -24,6 +24,7 @@ pub fn render(frame: &mut Frame, app: &App) {
|
||||||
Screen::ReassignProject => render_reassign_project(frame, app),
|
Screen::ReassignProject => render_reassign_project(frame, app),
|
||||||
Screen::LogView => render_log_view(frame, app),
|
Screen::LogView => render_log_view(frame, app),
|
||||||
Screen::LogViewHelp => render_log_view_help(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:",
|
"Actions:",
|
||||||
"- e: Edit the selected entry (Entry level only)",
|
"- 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)",
|
"- x: Delete the selected entry (Entry level only)",
|
||||||
"- b: Backfill - set entry start time to previous entry's end time",
|
"- b: Backfill - set entry start time to previous entry's end time",
|
||||||
" (Entry level, By Date view only)",
|
" (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);
|
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]);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue