Compare commits

..

2 Commits

Author SHA1 Message Date
d4aa36f9b9 Write out status to file (resolves #6) 2024-07-10 20:45:02 -04:00
10b5511ff9 Ability to pause session to resolve #8 2024-07-10 20:31:16 -04:00
3 changed files with 127 additions and 60 deletions

View File

@@ -7,6 +7,7 @@ edition = "2021"
anyhow = "1.0.86" anyhow = "1.0.86"
ratatui = "0.27.0" ratatui = "0.27.0"
regex = "1.10.5" regex = "1.10.5"
shellexpand = "3.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

@@ -1,11 +1,15 @@
pub const APP_TITLE: &str = "AntiDrift"; pub const APP_TITLE: &str = "AntiDrift";
pub const INTENTION_TITLE: &str = "Intention"; pub const DEFAULT_DURATION: &str = "25";
pub const DURATION_TITLE: &str = "Duration"; pub const DURATION_TITLE: &str = "Duration";
pub const INTENTION_TITLE: &str = "Intention";
pub const PAUSED: &str = "paused";
pub const PREVIOUS_SESSIONS_TITLE: &str = "Previous Sessions"; pub const PREVIOUS_SESSIONS_TITLE: &str = "Previous Sessions";
pub const SESSION_STATS_TITLE: &str = "Session Stats";
pub const STATUS_TITLE: &str = "Status";
pub const PROVIDE_INTENTION: &str = "Provide intention! "; pub const PROVIDE_INTENTION: &str = "Provide intention! ";
pub const PROVIDE_VALID_DURATION: &str = "Provide valid duration in minutes! "; pub const PROVIDE_VALID_DURATION: &str = "Provide valid duration in minutes! ";
pub const RATE_TITLES: &str = "Press 1, 2, 3 to rate titles!";
pub const READY_TO_START: &str = "Ready to start next session."; pub const READY_TO_START: &str = "Ready to start next session.";
pub const SESSION_IN_PROGRESS: &str = "Session In-Progress"; pub const SESSION_IN_PROGRESS: &str = "Session In-Progress";
pub const RATE_TITLES: &str = "Press 1, 2, 3 to rate titles!"; pub const SESSION_PAUSED: &str = "Session is paused. Unpause with 'p'.";
pub const SESSION_STATS_TITLE: &str = "Session Stats";
pub const STATUS_FILE: &str = "~/.antidrift_status";
pub const STATUS_TITLE: &str = "Status";

View File

@@ -1,6 +1,7 @@
use anyhow::Result; use anyhow::Result;
use shellexpand;
use std::collections::HashMap; use std::collections::HashMap;
use std::io::stdout; use std::io::{stdout, Write};
use std::rc::Rc; use std::rc::Rc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
mod constants; mod constants;
@@ -25,6 +26,7 @@ enum State {
InputIntention, InputIntention,
InputDuration, InputDuration,
InProgress, InProgress,
Paused,
End, End,
ShouldQuit, ShouldQuit,
} }
@@ -96,13 +98,13 @@ impl App {
App { App {
state: State::InputIntention, state: State::InputIntention,
user_intention: String::new(), user_intention: String::new(),
user_duration_str: "25".to_string(), user_duration_str: constants::DEFAULT_DURATION.to_string(),
user_duration: Duration::new(0, 0), user_duration: Duration::ZERO,
current_window_title: window_title.into(), current_window_title: window_title.into(),
current_window_time: Instant::now(), current_window_time: Instant::now(),
session_start: Instant::now(), session_start: Instant::now(),
session_stats: HashMap::new(), session_stats: HashMap::new(),
session_remaining: Duration::new(0, 0), session_remaining: Duration::ZERO,
session_ratings: Vec::new(), session_ratings: Vec::new(),
session_ratings_index: 0, session_ratings_index: 0,
session_results: Vec::new(), session_results: Vec::new(),
@@ -136,6 +138,36 @@ impl App {
self.session_ratings = session_stats_as_vec(&self.session_stats); self.session_ratings = session_stats_as_vec(&self.session_stats);
} }
fn cleanup(&self) {
let path = shellexpand::tilde(constants::STATUS_FILE).to_string();
let _ = std::fs::remove_file(path);
}
fn write_status(&self) {
let status = match self.state {
State::InProgress => format!(
"{} - {}",
self.user_intention,
duration_as_str(&self.session_remaining)
),
State::Paused => format!(
"antidrift paused - {}",
duration_as_str(&self.session_remaining)
),
_ => format!("antidrift inactive"),
};
let path = shellexpand::tilde(constants::STATUS_FILE).to_string();
if let Ok(mut file) = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&path)
{
let _ = file.write_all(status.as_bytes());
}
}
fn tick_50ms(&mut self) { fn tick_50ms(&mut self) {
match self.state { match self.state {
State::InputIntention | State::InputDuration => { State::InputIntention | State::InputDuration => {
@@ -152,11 +184,15 @@ impl App {
self.session_remaining = self.user_duration.saturating_sub(elapsed); self.session_remaining = self.user_duration.saturating_sub(elapsed);
update_session_stats(self); update_session_stats(self);
if self.session_remaining == Duration::new(0, 0) { if self.session_remaining.is_zero() {
self.to_end(); self.to_end();
} }
} }
State::ShouldQuit => {} State::ShouldQuit => {}
State::Paused => {
window::minimize_other(&constants::APP_TITLE);
update_session_stats(self);
}
State::End => { State::End => {
window::minimize_other(&constants::APP_TITLE); window::minimize_other(&constants::APP_TITLE);
} }
@@ -167,6 +203,11 @@ impl App {
fn tick_1s(&mut self) { fn tick_1s(&mut self) {
self.last_tick_1s = Instant::now(); self.last_tick_1s = Instant::now();
self.write_status();
if self.state == State::Paused {
self.user_duration = self.user_duration.saturating_add(Duration::from_secs(1));
}
} }
fn timeout(&self) -> Duration { fn timeout(&self) -> Duration {
@@ -242,7 +283,7 @@ fn session_stats_as_vec(session_stats: &HashMap<Rc<String>, Duration>) -> Vec<Se
fn main() -> Result<()> { fn main() -> Result<()> {
enable_raw_mode()?; enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?; stdout().execute(EnterAlternateScreen)?;
execute!(stdout(), SetTitle("AntiDrift"))?; execute!(stdout(), SetTitle(constants::APP_TITLE))?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?; let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
let mut app = App::new(); let mut app = App::new();
@@ -252,6 +293,7 @@ fn main() -> Result<()> {
app.handle_ticks(); app.handle_ticks();
} }
app.cleanup();
disable_raw_mode()?; disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?; stdout().execute(LeaveAlternateScreen)?;
Ok(()) Ok(())
@@ -274,8 +316,8 @@ fn handle_events(app: &mut App) -> Result<()> {
app.state = State::ShouldQuit; app.state = State::ShouldQuit;
} }
if app.state == State::InputIntention { match app.state {
match key.code { State::InputIntention => match key.code {
KeyCode::Enter => app.state = State::InputDuration, KeyCode::Enter => app.state = State::InputDuration,
KeyCode::Tab => app.state = State::InputDuration, KeyCode::Tab => app.state = State::InputDuration,
KeyCode::Backspace => { KeyCode::Backspace => {
@@ -285,9 +327,8 @@ fn handle_events(app: &mut App) -> Result<()> {
app.user_intention.push(c); app.user_intention.push(c);
} }
_ => {} _ => {}
} },
} else if app.state == State::InputDuration { State::InputDuration => match key.code {
match key.code {
KeyCode::Enter => app.to_in_progress(), KeyCode::Enter => app.to_in_progress(),
KeyCode::Tab => app.state = State::InputIntention, KeyCode::Tab => app.state = State::InputIntention,
KeyCode::Backspace => { KeyCode::Backspace => {
@@ -297,12 +338,14 @@ fn handle_events(app: &mut App) -> Result<()> {
app.user_duration_str.push(c); app.user_duration_str.push(c);
} }
_ => {} _ => {}
} },
} else if app.state == State::InProgress { State::InProgress => match key.code {
match key.code {
KeyCode::Char('q') => { KeyCode::Char('q') => {
app.to_end(); app.to_end();
} }
KeyCode::Char('p') => {
app.state = State::Paused;
}
KeyCode::Char('a') => { KeyCode::Char('a') => {
app.user_duration = app.user_duration.saturating_add(Duration::from_secs(60)); app.user_duration = app.user_duration.saturating_add(Duration::from_secs(60));
} }
@@ -310,31 +353,38 @@ fn handle_events(app: &mut App) -> Result<()> {
app.user_duration = app.user_duration.saturating_sub(Duration::from_secs(60)); app.user_duration = app.user_duration.saturating_sub(Duration::from_secs(60));
} }
_ => {} _ => {}
},
State::Paused => {
if key.code == KeyCode::Char('p') {
app.state = State::InProgress;
}
} }
} else if app.state == State::End { State::ShouldQuit => (),
let code = match key.code { State::End => {
KeyCode::Char('1') => 1, let code = match key.code {
KeyCode::Char('2') => 2, KeyCode::Char('1') => 1,
KeyCode::Char('3') => 3, KeyCode::Char('2') => 2,
_ => 0, KeyCode::Char('3') => 3,
}; _ => 0,
if app.session_ratings_index < app.session_ratings.len() && code != 0 {
app.session_ratings[app.session_ratings_index].rating = code;
app.session_ratings_index += 1;
}
if app.session_ratings_index >= app.session_ratings.len() {
app.state = State::InputIntention;
let mut session_result = SessionResult {
intention: app.user_intention.clone(),
duration: app.session_start.elapsed(),
session_ratings: std::mem::take(&mut app.session_ratings),
rating: 0,
rating_f64: 0.0,
}; };
session_result.rate();
app.session_results.push(session_result); if app.session_ratings_index < app.session_ratings.len() && code != 0 {
app.session_ratings[app.session_ratings_index].rating = code;
app.session_ratings_index += 1;
}
if app.session_ratings_index >= app.session_ratings.len() {
app.state = State::InputIntention;
let mut session_result = SessionResult {
intention: app.user_intention.clone(),
duration: app.session_start.elapsed(),
session_ratings: std::mem::take(&mut app.session_ratings),
rating: 0,
rating_f64: 0.0,
};
session_result.rate();
app.session_results.push(session_result);
}
} }
} }
@@ -342,7 +392,12 @@ fn handle_events(app: &mut App) -> Result<()> {
} }
fn update_session_stats(app: &mut App) { fn update_session_stats(app: &mut App) {
let window_title = window::get_title_clean().into(); let window_title = if app.state == State::Paused {
constants::PAUSED.to_string().into()
} else {
window::get_title_clean().into()
};
let delta = app.current_window_time.elapsed(); let delta = app.current_window_time.elapsed();
if app.current_window_title != window_title || (delta > Duration::from_secs(1)) { if app.current_window_title != window_title || (delta > Duration::from_secs(1)) {
let entry = app let entry = app
@@ -381,11 +436,14 @@ fn ui(frame: &mut Frame, app: &App) {
layout_intention, layout_intention,
); );
let input_duration: Vec<Line> = if app.state == State::InProgress { let input_duration: Vec<Line> = match app.state {
let s = duration_as_str(&app.session_remaining); State::InProgress | State::Paused => {
vec![Line::from(Span::raw(s))] let s = duration_as_str(&app.session_remaining);
} else { vec![Line::from(Span::raw(s))]
vec![Line::from(Span::raw(&app.user_duration_str))] }
_ => {
vec![Line::from(Span::raw(&app.user_duration_str))]
}
}; };
let border_type_duration = if app.state == State::InputDuration { let border_type_duration = if app.state == State::InputDuration {
BorderType::Thick BorderType::Thick
@@ -425,7 +483,7 @@ fn ui(frame: &mut Frame, app: &App) {
spans.push(span); spans.push(span);
} }
if app.user_duration == Duration::ZERO { if app.user_duration.is_zero() {
let span = Span::styled( let span = Span::styled(
constants::PROVIDE_VALID_DURATION, constants::PROVIDE_VALID_DURATION,
Style::new().fg(Color::LightRed), Style::new().fg(Color::LightRed),
@@ -450,6 +508,10 @@ fn ui(frame: &mut Frame, app: &App) {
); );
spans.push(span); spans.push(span);
} }
State::Paused => {
let span = Span::styled(constants::SESSION_PAUSED, Style::new().fg(Color::LightBlue));
spans.push(span);
}
State::ShouldQuit => {} State::ShouldQuit => {}
State::End => { State::End => {
let span = Span::styled(constants::RATE_TITLES, Style::new().fg(Color::LightBlue)); let span = Span::styled(constants::RATE_TITLES, Style::new().fg(Color::LightBlue));