Compare commits
2 Commits
4fbd53efa5
...
d4aa36f9b9
| Author | SHA1 | Date | |
|---|---|---|---|
| d4aa36f9b9 | |||
| 10b5511ff9 |
@@ -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"] }
|
||||||
@@ -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";
|
||||||
|
|||||||
150
src/main.rs
150
src/main.rs
@@ -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));
|
||||||
|
|||||||
Reference in New Issue
Block a user