Add 'view' page for log
This commit is contained in:
parent
cae499f3ee
commit
3220ff50cb
4 changed files with 194 additions and 15 deletions
|
|
@ -20,6 +20,7 @@
|
||||||
- **Timer control**: `Enter` toggles the selected item. Starting a different item automatically stops the previous timer.
|
- **Timer control**: `Enter` toggles the selected item. Starting a different item automatically stops the previous timer.
|
||||||
- **New entries**: `n` launches a modal (task name + optional project tag). Item is added to the current pane.
|
- **New entries**: `n` launches a modal (task name + optional project tag). Item is added to the current pane.
|
||||||
- **Project reassignment**: `p` opens a modal to change the project tag for the selected item.
|
- **Project reassignment**: `p` opens a modal to change the project tag for the selected item.
|
||||||
|
- **Log viewing**: `v` opens Watson's log viewer with daily/weekly/monthly views. Use `d`/`w`/`m` to switch periods, `j`/`k` to scroll.
|
||||||
- **Deletion**: `d` removes the selected entry; no noisy status message.
|
- **Deletion**: `d` removes the selected entry; no noisy status message.
|
||||||
- **Config editing**:
|
- **Config editing**:
|
||||||
- `Ctrl+e` edits task config (`state.yaml`).
|
- `Ctrl+e` edits task config (`state.yaml`).
|
||||||
|
|
@ -55,6 +56,7 @@ Requires `watson` on your `PATH` with a configured workspace.
|
||||||
| `Enter` | Start/stop timer |
|
| `Enter` | Start/stop timer |
|
||||||
| `n` | New entry (task + optional project) |
|
| `n` | New entry (task + optional project) |
|
||||||
| `p` | Reassign project for selected item |
|
| `p` | Reassign project for selected item |
|
||||||
|
| `v` | View Watson log (d/w/m for day/week/month) |
|
||||||
| `d` | Delete entry |
|
| `d` | Delete entry |
|
||||||
| `Ctrl+e` | Edit task config (`state.yaml`) |
|
| `Ctrl+e` | Edit task config (`state.yaml`) |
|
||||||
| `c` | Edit app config (`config.yaml`) or show config help |
|
| `c` | Edit app config (`config.yaml`) or show config help |
|
||||||
|
|
|
||||||
120
src/app.rs
120
src/app.rs
|
|
@ -10,19 +10,29 @@ pub enum Screen {
|
||||||
ConfigHelp,
|
ConfigHelp,
|
||||||
NewEntry,
|
NewEntry,
|
||||||
ReassignProject,
|
ReassignProject,
|
||||||
|
LogView,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum LogViewPeriod {
|
||||||
|
Day,
|
||||||
|
Week,
|
||||||
|
Month,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub state: AppState,
|
pub state: AppState,
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
pub current_screen: Screen,
|
pub current_screen: Screen,
|
||||||
pub needs_redraw: bool,
|
pub needs_clear: bool,
|
||||||
pub new_entry_buffer: String,
|
pub new_entry_buffer: String,
|
||||||
pub new_entry_project: String,
|
pub new_entry_project: String,
|
||||||
pub new_entry_cursor: usize,
|
pub new_entry_cursor: usize,
|
||||||
pub new_entry_mode: NewEntryMode, // Task or Project
|
pub new_entry_mode: NewEntryMode, // Task or Project
|
||||||
pub reassign_project_buffer: String,
|
pub reassign_project_buffer: String,
|
||||||
pub reassign_project_cursor: usize,
|
pub reassign_project_cursor: usize,
|
||||||
|
pub log_view_period: LogViewPeriod,
|
||||||
|
pub log_view_content: Vec<String>,
|
||||||
|
pub log_view_scroll: usize,
|
||||||
pub status_message: Option<(String, std::time::Instant)>,
|
pub status_message: Option<(String, std::time::Instant)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,13 +47,16 @@ impl App {
|
||||||
state: AppState::load()?,
|
state: AppState::load()?,
|
||||||
config: Config::load()?,
|
config: Config::load()?,
|
||||||
current_screen: Screen::Main,
|
current_screen: Screen::Main,
|
||||||
needs_redraw: true,
|
needs_clear: false,
|
||||||
new_entry_buffer: String::new(),
|
new_entry_buffer: String::new(),
|
||||||
new_entry_project: String::new(),
|
new_entry_project: String::new(),
|
||||||
new_entry_cursor: 0,
|
new_entry_cursor: 0,
|
||||||
new_entry_mode: NewEntryMode::Task,
|
new_entry_mode: NewEntryMode::Task,
|
||||||
reassign_project_buffer: String::new(),
|
reassign_project_buffer: String::new(),
|
||||||
reassign_project_cursor: 0,
|
reassign_project_cursor: 0,
|
||||||
|
log_view_period: LogViewPeriod::Day,
|
||||||
|
log_view_content: Vec::new(),
|
||||||
|
log_view_scroll: 0,
|
||||||
status_message: None,
|
status_message: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -52,13 +65,24 @@ impl App {
|
||||||
// Update status message
|
// Update status message
|
||||||
self.update_status_message();
|
self.update_status_message();
|
||||||
|
|
||||||
match self.current_screen {
|
let previous_screen = std::mem::discriminant(&self.current_screen);
|
||||||
|
|
||||||
|
let result = match self.current_screen {
|
||||||
Screen::Main => self.handle_main_event(event),
|
Screen::Main => self.handle_main_event(event),
|
||||||
Screen::Help => self.handle_help_event(event),
|
Screen::Help => self.handle_help_event(event),
|
||||||
Screen::ConfigHelp => self.handle_config_help_event(event),
|
Screen::ConfigHelp => self.handle_config_help_event(event),
|
||||||
Screen::NewEntry => self.handle_new_entry_event(event),
|
Screen::NewEntry => self.handle_new_entry_event(event),
|
||||||
Screen::ReassignProject => self.handle_reassign_project_event(event),
|
Screen::ReassignProject => self.handle_reassign_project_event(event),
|
||||||
|
Screen::LogView => self.handle_log_view_event(event),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_main_event(&mut self, event: Event) -> anyhow::Result<bool> {
|
fn handle_main_event(&mut self, event: Event) -> anyhow::Result<bool> {
|
||||||
|
|
@ -79,6 +103,7 @@ impl App {
|
||||||
(KeyCode::Char('c'), _) => self.edit_app_config()?,
|
(KeyCode::Char('c'), _) => self.edit_app_config()?,
|
||||||
(KeyCode::Char('n'), _) => self.start_new_entry(),
|
(KeyCode::Char('n'), _) => self.start_new_entry(),
|
||||||
(KeyCode::Char('p'), _) => self.start_reassign_project(),
|
(KeyCode::Char('p'), _) => self.start_reassign_project(),
|
||||||
|
(KeyCode::Char('v'), _) => self.start_log_view()?,
|
||||||
(KeyCode::Char('d'), _) => self.delete_current_item()?,
|
(KeyCode::Char('d'), _) => self.delete_current_item()?,
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
|
|
@ -244,6 +269,56 @@ impl App {
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
self.needs_clear = true;
|
||||||
|
}
|
||||||
|
KeyCode::Char('d') => {
|
||||||
|
self.log_view_period = LogViewPeriod::Day;
|
||||||
|
self.log_view_scroll = 0;
|
||||||
|
self.load_log_content()?;
|
||||||
|
self.needs_clear = true;
|
||||||
|
}
|
||||||
|
KeyCode::Char('w') => {
|
||||||
|
self.log_view_period = LogViewPeriod::Week;
|
||||||
|
self.log_view_scroll = 0;
|
||||||
|
self.load_log_content()?;
|
||||||
|
self.needs_clear = true;
|
||||||
|
}
|
||||||
|
KeyCode::Char('m') => {
|
||||||
|
self.log_view_period = LogViewPeriod::Month;
|
||||||
|
self.log_view_scroll = 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char('k') | KeyCode::Up => {
|
||||||
|
if self.log_view_scroll > 0 {
|
||||||
|
self.log_view_scroll -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::PageDown => {
|
||||||
|
self.log_view_scroll = (self.log_view_scroll + 10)
|
||||||
|
.min(self.log_view_content.len().saturating_sub(1));
|
||||||
|
}
|
||||||
|
KeyCode::PageUp => {
|
||||||
|
self.log_view_scroll = self.log_view_scroll.saturating_sub(10);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
fn move_selection(&mut self, delta: i32) {
|
fn move_selection(&mut self, delta: i32) {
|
||||||
let items_len = match self.state.current_pane {
|
let items_len = match self.state.current_pane {
|
||||||
0 => self.state.permanent_items.len(),
|
0 => self.state.permanent_items.len(),
|
||||||
|
|
@ -379,6 +454,37 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
};
|
||||||
|
|
||||||
|
let output = Command::new("watson").arg("log").arg(flag).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();
|
||||||
|
} else {
|
||||||
|
let error = String::from_utf8_lossy(&output.stderr);
|
||||||
|
self.log_view_content = vec![
|
||||||
|
"Failed to load Watson log:".to_string(),
|
||||||
|
error.to_string(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn reassign_project_for_current_item(&mut self) -> anyhow::Result<()> {
|
fn reassign_project_for_current_item(&mut self) -> anyhow::Result<()> {
|
||||||
let items = match self.state.current_pane {
|
let items = match self.state.current_pane {
|
||||||
0 => &mut self.state.permanent_items,
|
0 => &mut self.state.permanent_items,
|
||||||
|
|
@ -439,14 +545,14 @@ impl App {
|
||||||
if status.success() {
|
if status.success() {
|
||||||
// Reload entire application state
|
// Reload entire application state
|
||||||
self.config = Config::load()?;
|
self.config = Config::load()?;
|
||||||
// Signal for complete reload
|
|
||||||
self.current_screen = Screen::Main;
|
self.current_screen = Screen::Main;
|
||||||
self.needs_redraw = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return to TUI mode
|
// Return to TUI mode
|
||||||
execute!(stdout(), EnterAlternateScreen)?;
|
execute!(stdout(), EnterAlternateScreen)?;
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
|
|
||||||
|
self.needs_clear = true;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -476,14 +582,14 @@ impl App {
|
||||||
if status.success() {
|
if status.success() {
|
||||||
// Reload entire application state
|
// Reload entire application state
|
||||||
self.state = AppState::load()?;
|
self.state = AppState::load()?;
|
||||||
// Signal for complete reload
|
|
||||||
self.current_screen = Screen::Main;
|
self.current_screen = Screen::Main;
|
||||||
self.needs_redraw = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return to TUI mode
|
// Return to TUI mode
|
||||||
execute!(stdout(), EnterAlternateScreen)?;
|
execute!(stdout(), EnterAlternateScreen)?;
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
|
|
||||||
|
self.needs_clear = true;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
src/main.rs
13
src/main.rs
|
|
@ -42,19 +42,18 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut app::App) -> Result<()> {
|
fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut app::App) -> Result<()> {
|
||||||
loop {
|
loop {
|
||||||
// Force a redraw if needed
|
// Clear terminal if we switched screens
|
||||||
if app.needs_redraw {
|
if app.needs_clear {
|
||||||
terminal.clear()?; // Clear the entire screen
|
terminal.clear()?;
|
||||||
terminal.draw(|f| ui::render(f, app))?;
|
app.needs_clear = false;
|
||||||
app.needs_redraw = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
terminal.draw(|f| ui::render(f, app))?;
|
||||||
|
|
||||||
if event::poll(std::time::Duration::from_millis(50))? {
|
if event::poll(std::time::Duration::from_millis(50))? {
|
||||||
if app.handle_event(event::read()?)? {
|
if app.handle_event(event::read()?)? {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
// Always redraw after any event
|
|
||||||
terminal.draw(|f| ui::render(f, app))?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
74
src/ui.rs
74
src/ui.rs
|
|
@ -7,7 +7,7 @@ use ratatui::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{App, NewEntryMode, Screen},
|
app::{App, LogViewPeriod, NewEntryMode, Screen},
|
||||||
state::{AppState, TimeItem},
|
state::{AppState, TimeItem},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -21,6 +21,7 @@ pub fn render(frame: &mut Frame, app: &App) {
|
||||||
Screen::ConfigHelp => render_config_help(frame, app),
|
Screen::ConfigHelp => render_config_help(frame, app),
|
||||||
Screen::NewEntry => render_new_entry(frame, app),
|
Screen::NewEntry => render_new_entry(frame, app),
|
||||||
Screen::ReassignProject => render_reassign_project(frame, app),
|
Screen::ReassignProject => render_reassign_project(frame, app),
|
||||||
|
Screen::LogView => render_log_view(frame, app),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,6 +212,7 @@ fn render_bottom_bar(frame: &mut Frame, area: Rect, app: &App) {
|
||||||
("c", "config"),
|
("c", "config"),
|
||||||
("n", "new"),
|
("n", "new"),
|
||||||
("p", "project"),
|
("p", "project"),
|
||||||
|
("v", "log"),
|
||||||
("d", "delete"),
|
("d", "delete"),
|
||||||
("q", "quit"),
|
("q", "quit"),
|
||||||
];
|
];
|
||||||
|
|
@ -314,6 +316,7 @@ fn render_help(frame: &mut Frame, _app: &App) {
|
||||||
"Enter - Start/stop timer",
|
"Enter - Start/stop timer",
|
||||||
"d - Delete task",
|
"d - Delete task",
|
||||||
"p - Reassign project",
|
"p - Reassign project",
|
||||||
|
"v - View Watson log",
|
||||||
"Ctrl+e - Edit tasks config",
|
"Ctrl+e - Edit tasks config",
|
||||||
"c - Edit app config",
|
"c - Edit app config",
|
||||||
"n - New task",
|
"n - New task",
|
||||||
|
|
@ -503,3 +506,72 @@ fn render_reassign_project(frame: &mut Frame, app: &App) {
|
||||||
|
|
||||||
frame.render_widget(help_text, chunks[1]);
|
frame.render_widget(help_text, chunks[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_log_view(frame: &mut Frame, app: &App) {
|
||||||
|
let area = frame.size();
|
||||||
|
|
||||||
|
// Create the main content area (leave space for command bar at bottom)
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Min(3), Constraint::Length(1)])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
let period_str = match app.log_view_period {
|
||||||
|
LogViewPeriod::Day => "Day",
|
||||||
|
LogViewPeriod::Week => "Week",
|
||||||
|
LogViewPeriod::Month => "Month",
|
||||||
|
};
|
||||||
|
|
||||||
|
let title = format!("Watson Log - {} View", period_str);
|
||||||
|
|
||||||
|
let block = Block::default()
|
||||||
|
.title(title)
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.style(Style::default().fg(ACTIVE_COLOR).bg(Color::Reset));
|
||||||
|
|
||||||
|
// Calculate visible area for content
|
||||||
|
let inner_area = block.inner(chunks[0]);
|
||||||
|
let visible_lines = inner_area.height as usize;
|
||||||
|
|
||||||
|
// Get the visible slice of content
|
||||||
|
let start = app.log_view_scroll;
|
||||||
|
let end = (start + visible_lines).min(app.log_view_content.len());
|
||||||
|
let visible_content: Vec<String> = app.log_view_content[start..end].to_vec();
|
||||||
|
|
||||||
|
let text = if visible_content.is_empty() {
|
||||||
|
"No log entries for this period.".to_string()
|
||||||
|
} else {
|
||||||
|
visible_content.join("\n")
|
||||||
|
};
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new(text)
|
||||||
|
.block(block)
|
||||||
|
.style(Style::default().fg(Color::White))
|
||||||
|
.wrap(ratatui::widgets::Wrap { trim: false });
|
||||||
|
|
||||||
|
frame.render_widget(paragraph, chunks[0]);
|
||||||
|
|
||||||
|
// Render command bar
|
||||||
|
let commands = vec![
|
||||||
|
("d", "day"),
|
||||||
|
("w", "week"),
|
||||||
|
("m", "month"),
|
||||||
|
("j/k", "scroll"),
|
||||||
|
("q/ESC", "back"),
|
||||||
|
];
|
||||||
|
|
||||||
|
let command_text = format!(
|
||||||
|
" {}",
|
||||||
|
commands
|
||||||
|
.iter()
|
||||||
|
.map(|(key, desc)| format!("{} ({})", key, desc))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" · ")
|
||||||
|
);
|
||||||
|
|
||||||
|
let command_bar = Paragraph::new(command_text)
|
||||||
|
.style(Style::default().fg(Color::White))
|
||||||
|
.alignment(Alignment::Left);
|
||||||
|
|
||||||
|
frame.render_widget(command_bar, chunks[1]);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue