Edit and delete entries from view page
This commit is contained in:
parent
3220ff50cb
commit
605d5f5792
5 changed files with 229 additions and 33 deletions
181
src/app.rs
181
src/app.rs
|
|
@ -2,8 +2,18 @@ use crate::config::Config;
|
|||
use crate::state::{AppState, TimeItem};
|
||||
use chrono::Utc;
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
use serde::Deserialize;
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub(crate) struct WatsonFrame {
|
||||
id: String,
|
||||
project: String,
|
||||
start: String,
|
||||
stop: String,
|
||||
tags: Vec<String>,
|
||||
}
|
||||
|
||||
pub enum Screen {
|
||||
Main,
|
||||
Help,
|
||||
|
|
@ -31,8 +41,10 @@ pub struct App {
|
|||
pub reassign_project_buffer: String,
|
||||
pub reassign_project_cursor: usize,
|
||||
pub log_view_period: LogViewPeriod,
|
||||
pub log_view_frames: Vec<WatsonFrame>,
|
||||
pub log_view_content: Vec<String>,
|
||||
pub log_view_scroll: usize,
|
||||
pub log_view_selected: usize,
|
||||
pub status_message: Option<(String, std::time::Instant)>,
|
||||
}
|
||||
|
||||
|
|
@ -55,8 +67,10 @@ impl App {
|
|||
reassign_project_buffer: String::new(),
|
||||
reassign_project_cursor: 0,
|
||||
log_view_period: LogViewPeriod::Day,
|
||||
log_view_frames: Vec::new(),
|
||||
log_view_content: Vec::new(),
|
||||
log_view_scroll: 0,
|
||||
log_view_selected: 0,
|
||||
status_message: None,
|
||||
})
|
||||
}
|
||||
|
|
@ -275,42 +289,52 @@ impl App {
|
|||
KeyCode::Esc | KeyCode::Char('q') => {
|
||||
self.current_screen = Screen::Main;
|
||||
self.log_view_scroll = 0;
|
||||
self.log_view_selected = 0;
|
||||
self.needs_clear = true;
|
||||
}
|
||||
KeyCode::Char('d') => {
|
||||
self.log_view_period = LogViewPeriod::Day;
|
||||
self.log_view_scroll = 0;
|
||||
self.log_view_selected = 0;
|
||||
self.load_log_content()?;
|
||||
self.needs_clear = true;
|
||||
}
|
||||
KeyCode::Char('w') => {
|
||||
self.log_view_period = LogViewPeriod::Week;
|
||||
self.log_view_scroll = 0;
|
||||
self.log_view_selected = 0;
|
||||
self.load_log_content()?;
|
||||
self.needs_clear = true;
|
||||
}
|
||||
KeyCode::Char('m') => {
|
||||
self.log_view_period = LogViewPeriod::Month;
|
||||
self.log_view_scroll = 0;
|
||||
self.log_view_selected = 0;
|
||||
self.load_log_content()?;
|
||||
self.needs_clear = true;
|
||||
}
|
||||
KeyCode::Char('j') | KeyCode::Down => {
|
||||
if self.log_view_scroll < self.log_view_content.len().saturating_sub(1) {
|
||||
self.log_view_scroll += 1;
|
||||
if self.log_view_selected < self.log_view_frames.len().saturating_sub(1) {
|
||||
self.log_view_selected += 1;
|
||||
}
|
||||
}
|
||||
KeyCode::Char('k') | KeyCode::Up => {
|
||||
if self.log_view_scroll > 0 {
|
||||
self.log_view_scroll -= 1;
|
||||
if self.log_view_selected > 0 {
|
||||
self.log_view_selected -= 1;
|
||||
}
|
||||
}
|
||||
KeyCode::Char('e') => {
|
||||
self.edit_selected_frame()?;
|
||||
}
|
||||
KeyCode::Char('x') => {
|
||||
self.delete_selected_frame()?;
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
self.log_view_scroll = (self.log_view_scroll + 10)
|
||||
.min(self.log_view_content.len().saturating_sub(1));
|
||||
self.log_view_selected = (self.log_view_selected + 10)
|
||||
.min(self.log_view_frames.len().saturating_sub(1));
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
self.log_view_scroll = self.log_view_scroll.saturating_sub(10);
|
||||
self.log_view_selected = self.log_view_selected.saturating_sub(10);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
|
@ -319,6 +343,124 @@ impl App {
|
|||
Ok(false)
|
||||
}
|
||||
|
||||
fn format_log_entries(&mut self) {
|
||||
use chrono::{DateTime, Local, Timelike};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
if self.log_view_frames.is_empty() {
|
||||
self.log_view_content = vec!["No log entries for this period.".to_string()];
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
fn move_selection(&mut self, delta: i32) {
|
||||
let items_len = match self.state.current_pane {
|
||||
0 => self.state.permanent_items.len(),
|
||||
|
|
@ -469,13 +611,32 @@ impl App {
|
|||
LogViewPeriod::Month => "--month",
|
||||
};
|
||||
|
||||
let output = Command::new("watson").arg("log").arg(flag).output()?;
|
||||
let output = Command::new("watson")
|
||||
.arg("log")
|
||||
.arg(flag)
|
||||
.arg("--json")
|
||||
.output()?;
|
||||
|
||||
if output.status.success() {
|
||||
let content = String::from_utf8_lossy(&output.stdout);
|
||||
self.log_view_content = content.lines().map(|s| s.to_string()).collect();
|
||||
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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
self.log_view_frames.clear();
|
||||
self.log_view_content = vec![
|
||||
"Failed to load Watson log:".to_string(),
|
||||
error.to_string(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue