Compare commits

...

2 Commits

Author SHA1 Message Date
a44d52b402 Only rate titles with more than 30 seconds 2025-12-29 17:32:06 -05:00
1a89906ae8 Implement session persistance 2025-12-22 19:09:09 -05:00
3 changed files with 37 additions and 4 deletions

View File

@@ -8,6 +8,8 @@ anyhow = "1.0.86"
ratatui = "0.27.0" ratatui = "0.27.0"
regex = "1.10.5" regex = "1.10.5"
shellexpand = "3.1.0" shellexpand = "3.1.0"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["winuser", "processthreadsapi"] } winapi = { version = "0.3", features = ["winuser", "processthreadsapi"] }

View File

@@ -12,6 +12,7 @@ pub const SESSION_IN_PROGRESS: &str = "Session In-Progress";
pub const SESSION_PAUSED: &str = "Session is paused. Unpause with 'p'."; pub const SESSION_PAUSED: &str = "Session is paused. Unpause with 'p'.";
pub const SESSION_STATS_TITLE: &str = "Session Stats"; pub const SESSION_STATS_TITLE: &str = "Session Stats";
pub const STATUS_FILE: &str = "~/.antidrift_status"; pub const STATUS_FILE: &str = "~/.antidrift_status";
pub const HISTORY_FILE: &str = "~/.antidrift_history.jsonl";
pub const STATUS_TITLE: &str = "Status"; pub const STATUS_TITLE: &str = "Status";
pub const STATUS_CONFIGURE: &str = "🔄 antidrift configure session"; pub const STATUS_CONFIGURE: &str = "🔄 antidrift configure session";
pub const STATUS_PAUSED: &str = "⏸ antidrift paused"; pub const STATUS_PAUSED: &str = "⏸ antidrift paused";

View File

@@ -1,9 +1,10 @@
use anyhow::Result; use anyhow::Result;
use serde::{Serialize, Serializer};
use shellexpand; use shellexpand;
use std::collections::HashMap; use std::collections::HashMap;
use std::io::{stdout, Write}; use std::io::{stdout, Write};
use std::rc::Rc; use std::rc::Rc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant}; // <--- Add this
mod constants; mod constants;
mod window; mod window;
@@ -31,8 +32,17 @@ enum State {
ShouldQuit, ShouldQuit,
} }
fn serialize_duration<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u64(duration.as_secs())
}
#[derive(Serialize)]
struct SessionResult { struct SessionResult {
intention: String, intention: String,
#[serde(serialize_with = "serialize_duration")]
duration: Duration, duration: Duration,
session_ratings: Vec<SessionRating>, session_ratings: Vec<SessionRating>,
rating: u8, rating: u8,
@@ -67,7 +77,7 @@ impl SessionResult {
} }
} }
#[derive(Clone)] #[derive(Clone, Serialize)]
struct SessionRating { struct SessionRating {
window_title: Rc<String>, window_title: Rc<String>,
duration: Duration, duration: Duration,
@@ -91,6 +101,24 @@ struct App {
last_tick_1s: Instant, last_tick_1s: Instant,
} }
fn save_session(session: &SessionResult) {
let path = shellexpand::tilde(constants::HISTORY_FILE).to_string();
// 1. Open file in append mode, create if missing
let file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path);
if let Ok(mut f) = file {
// 2. Serialize to JSON
if let Ok(json) = serde_json::to_string(session) {
// 3. Write newline appended
let _ = writeln!(f, "{}", json);
}
}
}
impl App { impl App {
fn new() -> Self { fn new() -> Self {
let window_title = window::get_title_clean(); let window_title = window::get_title_clean();
@@ -217,7 +245,7 @@ impl App {
Duration::from_millis(50).saturating_sub(self.last_tick_50ms.elapsed()) Duration::from_millis(50).saturating_sub(self.last_tick_50ms.elapsed())
} }
fn get_session_results(&self) -> Vec<Line> { fn get_session_results(&self) -> Vec<Line<'_>> {
self.session_results self.session_results
.iter() .iter()
.map(|r| { .map(|r| {
@@ -233,7 +261,7 @@ impl App {
.collect() .collect()
} }
fn get_session_stats(&self) -> Vec<Line> { fn get_session_stats(&self) -> Vec<Line<'_>> {
let mut zero_encountered = if self.state != State::End { let mut zero_encountered = if self.state != State::End {
true true
} else { } else {
@@ -273,6 +301,7 @@ fn duration_as_str(duration: &Duration) -> String {
fn session_stats_as_vec(session_stats: &HashMap<Rc<String>, Duration>) -> Vec<SessionRating> { fn session_stats_as_vec(session_stats: &HashMap<Rc<String>, Duration>) -> Vec<SessionRating> {
let mut stats: Vec<_> = session_stats let mut stats: Vec<_> = session_stats
.iter() .iter()
.filter(|(_, duration)| duration.as_secs() > 30)
.map(|(title, duration)| SessionRating { .map(|(title, duration)| SessionRating {
window_title: title.clone(), window_title: title.clone(),
duration: duration.clone(), duration: duration.clone(),
@@ -386,6 +415,7 @@ fn handle_events(app: &mut App) -> Result<()> {
rating_f64: 0.0, rating_f64: 0.0,
}; };
session_result.rate(); session_result.rate();
save_session(&session_result);
app.session_results.push(session_result); app.session_results.push(session_result);
} }
} }