Remove log stuff
This commit is contained in:
parent
ae045167bf
commit
7b0626cdf0
2 changed files with 68 additions and 77 deletions
67
README.md
Normal file
67
README.md
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
# wat
|
||||||
|
|
||||||
|
`wat` is a terminal UI wrapper around [Watson](https://tailordev.github.io/Watson/) that lets you browse and launch timers via a keyboard-driven interface.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Goals & Workflow
|
||||||
|
- Never type `watson start ...` again—start/stop timers with `Enter`.
|
||||||
|
- Maintain three panes simultaneously:
|
||||||
|
1. **Permanent Items** – curated recurring tasks defined in `state.yaml`.
|
||||||
|
2. **Recurring Items** – semi-regular tasks you hop between.
|
||||||
|
3. **Ad-Hoc Items** – last ~20 one-offs, automatically tracked.
|
||||||
|
- Highlight clearly when a timer is running (bold green) vs idle (yellow).
|
||||||
|
- Keep everything human-editable: YAML state/config under `~/.config/wat`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
- **Navigation**: `j/k` or arrows move within a pane; `h/l`, arrows, or `Ctrl+n/p` switch panes; `q` quits.
|
||||||
|
- **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.
|
||||||
|
- **Deletion**: `d` removes the selected entry; no noisy status message.
|
||||||
|
- **Config editing**:
|
||||||
|
- `Ctrl+e` edits task config (`state.yaml`).
|
||||||
|
- `c` edits app config (`config.yaml`).
|
||||||
|
- **Help overlays**: `?` for help, `c` inside help for config instructions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files & Persistence
|
||||||
|
```
|
||||||
|
~/.config/wat/
|
||||||
|
├─ state.yaml # serialized AppState (items, selections, active timer)
|
||||||
|
└─ config.yaml # UI/app options (help hints, allowed projects, etc.)
|
||||||
|
```
|
||||||
|
- `state.yaml` and `config.yaml` use `serde_yaml` for readability.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Building & Running
|
||||||
|
```
|
||||||
|
cargo build
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
Requires `watson` on your `PATH` with a configured workspace.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Keybindings (Summary)
|
||||||
|
| Keys | Action |
|
||||||
|
|------|--------|
|
||||||
|
| `j` / `k`, arrows | Move selection |
|
||||||
|
| `h` / `l`, arrows, `Ctrl+n` / `Ctrl+p` | Switch panes |
|
||||||
|
| `Enter` | Start/stop timer |
|
||||||
|
| `n` | New entry (task + optional project) |
|
||||||
|
| `d` | Delete entry |
|
||||||
|
| `Ctrl+e` | Edit task config (`state.yaml`) |
|
||||||
|
| `c` | Edit app config (`config.yaml`) or show config help |
|
||||||
|
| `?` | Help screen |
|
||||||
|
| `q` | Quit |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TODO / Follow-ups
|
||||||
|
- Add inline editing of permanent/recurring items instead of dropping to `$EDITOR`.
|
||||||
|
- Consider status messaging for critical errors (currently unused after deleting the delete confirmation).
|
||||||
|
- Add tests/snapshots for UI components.
|
||||||
78
src/state.rs
78
src/state.rs
|
|
@ -1,10 +1,6 @@
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::path::PathBuf;
|
||||||
fs::{self, OpenOptions},
|
|
||||||
io::Write,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct TimeItem {
|
pub struct TimeItem {
|
||||||
|
|
@ -57,12 +53,6 @@ impl AppState {
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log_file() -> anyhow::Result<PathBuf> {
|
|
||||||
let mut path = Self::config_dir()?;
|
|
||||||
path.push("watson.log");
|
|
||||||
Ok(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load() -> anyhow::Result<Self> {
|
pub fn load() -> anyhow::Result<Self> {
|
||||||
let path = Self::state_file()?;
|
let path = Self::state_file()?;
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
|
|
@ -86,19 +76,6 @@ impl AppState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_log_lines(limit: usize) -> anyhow::Result<Vec<String>> {
|
|
||||||
let path = Self::log_file()?;
|
|
||||||
if !path.exists() {
|
|
||||||
return Ok(Vec::new());
|
|
||||||
}
|
|
||||||
let contents = fs::read_to_string(path)?;
|
|
||||||
let mut lines: Vec<String> = contents.lines().map(|l| l.to_string()).collect();
|
|
||||||
if lines.len() > limit {
|
|
||||||
lines = lines.split_off(lines.len() - limit);
|
|
||||||
}
|
|
||||||
Ok(lines)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start_timer(&mut self, item: TimeItem) -> anyhow::Result<()> {
|
pub fn start_timer(&mut self, item: TimeItem) -> anyhow::Result<()> {
|
||||||
if let Some((_, _)) = self.active_timer {
|
if let Some((_, _)) = self.active_timer {
|
||||||
self.stop_timer()?;
|
self.stop_timer()?;
|
||||||
|
|
@ -119,10 +96,6 @@ impl AppState {
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = command.output()?;
|
let output = command.output()?;
|
||||||
Self::log_command_output(
|
|
||||||
&format!("watson start {} {}", item.name, item.tags.join(" ")),
|
|
||||||
&output,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
|
|
@ -139,8 +112,6 @@ impl AppState {
|
||||||
if let Some((ref item, _)) = self.active_timer {
|
if let Some((ref item, _)) = self.active_timer {
|
||||||
let output = std::process::Command::new("watson").arg("stop").output()?;
|
let output = std::process::Command::new("watson").arg("stop").output()?;
|
||||||
|
|
||||||
Self::log_command_output("watson stop", &output)?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"Watson stop failed: {}",
|
"Watson stop failed: {}",
|
||||||
|
|
@ -162,51 +133,4 @@ impl AppState {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log_command_output(description: &str, output: &std::process::Output) -> anyhow::Result<()> {
|
|
||||||
let mut lines = Vec::new();
|
|
||||||
lines.push(format!("[{}] {description}", Utc::now().to_rfc3339()));
|
|
||||||
lines.push(format!("status: {}", output.status));
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
||||||
if !stdout.is_empty() {
|
|
||||||
lines.push("stdout:".into());
|
|
||||||
lines.push(stdout);
|
|
||||||
}
|
|
||||||
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
|
||||||
if !stderr.is_empty() {
|
|
||||||
lines.push("stderr:".into());
|
|
||||||
lines.push(stderr);
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.push(String::new());
|
|
||||||
Self::append_log_entry(&lines)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn append_log_entry(lines: &[String]) -> anyhow::Result<()> {
|
|
||||||
let path = Self::log_file()?;
|
|
||||||
if let Some(parent) = path.parent() {
|
|
||||||
fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut file = OpenOptions::new().create(true).append(true).open(&path)?;
|
|
||||||
for line in lines {
|
|
||||||
writeln!(file, "{line}")?;
|
|
||||||
}
|
|
||||||
Self::trim_log(&path)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn trim_log(path: &Path) -> anyhow::Result<()> {
|
|
||||||
const MAX_LINES: usize = 800;
|
|
||||||
let contents = fs::read_to_string(path)?;
|
|
||||||
let mut lines: Vec<&str> = contents.lines().collect();
|
|
||||||
if lines.len() > MAX_LINES {
|
|
||||||
lines = lines.split_off(lines.len() - MAX_LINES);
|
|
||||||
fs::write(path, lines.join("\n") + "\n")?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue