Compare commits
13 Commits
95dcfa72d6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a44d52b402 | |||
| 1a89906ae8 | |||
| d84b83c6d5 | |||
| d4aa36f9b9 | |||
| 10b5511ff9 | |||
| 4fbd53efa5 | |||
| 0d50da7ceb | |||
| 288ef6a9c4 | |||
| f13dc7f10e | |||
| abad26d9d2 | |||
| 49bafa2567 | |||
| 860fe615ab | |||
| 38a03f063d |
@@ -7,3 +7,9 @@ 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"
|
||||||
|
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
winapi = { version = "0.3", features = ["winuser", "processthreadsapi"] }
|
||||||
|
|||||||
13
README.md
Normal file
13
README.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# AntiDrift
|
||||||
|
|
||||||
|
Just my personal productivity tool. It asks me about my intention for the next
|
||||||
|
work session. Until I have provided an intention, duration, and start a
|
||||||
|
session, it forcefully minimizes all windows. It then records all active
|
||||||
|
windows during the session. At the end, it allows me to rate how relevant each
|
||||||
|
window was and calculates a session score from that.
|
||||||
|
|
||||||
|
To use AntiDrift, run `cargo run --release` directly, or `cargo build --release`
|
||||||
|
and copy the binary into your `PATH`.
|
||||||
|
|
||||||
|
Under Linux, we use `xdotool` to get window titles and minimize windows. Under
|
||||||
|
Windows, we use the package `winapi` for the same functionality.
|
||||||
20
src/constants.rs
Normal file
20
src/constants.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
pub const APP_TITLE: &str = "AntiDrift";
|
||||||
|
pub const DEFAULT_DURATION: &str = "25";
|
||||||
|
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 PROVIDE_INTENTION: &str = "Provide intention! ";
|
||||||
|
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 SESSION_IN_PROGRESS: &str = "Session In-Progress";
|
||||||
|
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 HISTORY_FILE: &str = "~/.antidrift_history.jsonl";
|
||||||
|
pub const STATUS_TITLE: &str = "Status";
|
||||||
|
pub const STATUS_CONFIGURE: &str = "🔄 antidrift configure session";
|
||||||
|
pub const STATUS_PAUSED: &str = "⏸ antidrift paused";
|
||||||
|
pub const STATUS_RATE_SESSION: &str = "☑ rate antidrift session!";
|
||||||
|
pub const STATUS_QUIT: &str = "antidrift shutting down";
|
||||||
417
src/main.rs
417
src/main.rs
@@ -1,17 +1,24 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use serde::{Serialize, Serializer};
|
||||||
|
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}; // <--- Add this
|
||||||
|
mod constants;
|
||||||
mod window;
|
mod window;
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
crossterm::{
|
crossterm::{
|
||||||
event::{self, Event, KeyCode},
|
event::{self, Event, KeyCode},
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
execute,
|
||||||
|
terminal::{
|
||||||
|
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, SetTitle,
|
||||||
|
},
|
||||||
ExecutableCommand,
|
ExecutableCommand,
|
||||||
},
|
},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
|
style::Color,
|
||||||
widgets::*,
|
widgets::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -20,9 +27,63 @@ enum State {
|
|||||||
InputIntention,
|
InputIntention,
|
||||||
InputDuration,
|
InputDuration,
|
||||||
InProgress,
|
InProgress,
|
||||||
|
Paused,
|
||||||
|
End,
|
||||||
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 {
|
||||||
|
intention: String,
|
||||||
|
#[serde(serialize_with = "serialize_duration")]
|
||||||
|
duration: Duration,
|
||||||
|
session_ratings: Vec<SessionRating>,
|
||||||
|
rating: u8,
|
||||||
|
rating_f64: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionResult {
|
||||||
|
fn rate(&mut self) {
|
||||||
|
let mut rating = 0_f64;
|
||||||
|
let total_duration = self
|
||||||
|
.session_ratings
|
||||||
|
.iter()
|
||||||
|
.map(|r| r.duration)
|
||||||
|
.fold(Duration::ZERO, |acc, dur| acc.saturating_add(dur));
|
||||||
|
|
||||||
|
for session_rating in &self.session_ratings {
|
||||||
|
let ratio: f64 = session_rating.duration.as_secs_f64() / total_duration.as_secs_f64();
|
||||||
|
rating += (session_rating.rating as f64) * ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.rating_f64 = rating;
|
||||||
|
|
||||||
|
if rating > 2.5 {
|
||||||
|
self.rating = 3;
|
||||||
|
} else if rating > 2.0 {
|
||||||
|
self.rating = 2;
|
||||||
|
} else if rating > 1.0 {
|
||||||
|
self.rating = 1;
|
||||||
|
} else {
|
||||||
|
self.rating = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize)]
|
||||||
|
struct SessionRating {
|
||||||
|
window_title: Rc<String>,
|
||||||
|
duration: Duration,
|
||||||
|
rating: u8,
|
||||||
|
}
|
||||||
|
|
||||||
struct App {
|
struct App {
|
||||||
state: State,
|
state: State,
|
||||||
user_intention: String,
|
user_intention: String,
|
||||||
@@ -32,10 +93,32 @@ struct App {
|
|||||||
current_window_time: Instant,
|
current_window_time: Instant,
|
||||||
session_start: Instant,
|
session_start: Instant,
|
||||||
session_stats: HashMap<Rc<String>, Duration>,
|
session_stats: HashMap<Rc<String>, Duration>,
|
||||||
|
session_remaining: Duration,
|
||||||
|
session_ratings: Vec<SessionRating>,
|
||||||
|
session_ratings_index: usize,
|
||||||
|
session_results: Vec<SessionResult>,
|
||||||
last_tick_50ms: Instant,
|
last_tick_50ms: Instant,
|
||||||
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();
|
||||||
@@ -43,12 +126,16 @@ impl App {
|
|||||||
App {
|
App {
|
||||||
state: State::InputIntention,
|
state: State::InputIntention,
|
||||||
user_intention: String::new(),
|
user_intention: String::new(),
|
||||||
user_duration_str: String::new(),
|
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::ZERO,
|
||||||
|
session_ratings: Vec::new(),
|
||||||
|
session_ratings_index: 0,
|
||||||
|
session_results: Vec::new(),
|
||||||
last_tick_50ms: Instant::now(),
|
last_tick_50ms: Instant::now(),
|
||||||
last_tick_1s: Instant::now(),
|
last_tick_1s: Instant::now(),
|
||||||
}
|
}
|
||||||
@@ -64,27 +151,82 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn to_in_progress(&mut self) {
|
fn to_in_progress(&mut self) {
|
||||||
let Ok(user_duration) = self.user_duration_str.parse::<u64>() else {
|
if self.user_intention.len() == 0 || self.user_duration == Duration::ZERO {
|
||||||
// TODO: Print error to the user
|
|
||||||
return;
|
return;
|
||||||
};
|
}
|
||||||
|
|
||||||
self.user_duration = Duration::from_secs(user_duration * 60);
|
|
||||||
self.state = State::InProgress;
|
self.state = State::InProgress;
|
||||||
self.current_window_time = Instant::now();
|
self.current_window_time = Instant::now();
|
||||||
self.session_start = self.current_window_time;
|
self.session_start = self.current_window_time;
|
||||||
self.session_stats = HashMap::new();
|
self.session_stats = HashMap::new();
|
||||||
|
self.session_ratings_index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_end(&mut self) {
|
||||||
|
self.state = State::End;
|
||||||
|
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::InputIntention | State::InputDuration => constants::STATUS_CONFIGURE.to_string(),
|
||||||
|
State::InProgress => format!(
|
||||||
|
"🎯 {} - {}",
|
||||||
|
self.user_intention,
|
||||||
|
duration_as_str(&self.session_remaining)
|
||||||
|
),
|
||||||
|
State::Paused => format!(
|
||||||
|
"{} - {}",
|
||||||
|
constants::STATUS_PAUSED,
|
||||||
|
duration_as_str(&self.session_remaining)
|
||||||
|
),
|
||||||
|
State::End => constants::STATUS_RATE_SESSION.to_string(),
|
||||||
|
State::ShouldQuit => constants::STATUS_QUIT.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
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 => {
|
||||||
window::minimize_other("kitty");
|
if let Ok(user_duration_mins) = self.user_duration_str.parse::<u64>() {
|
||||||
|
self.user_duration = Duration::from_secs(user_duration_mins * 60);
|
||||||
|
} else {
|
||||||
|
self.user_duration = Duration::ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
window::minimize_other(&constants::APP_TITLE);
|
||||||
}
|
}
|
||||||
State::InProgress => {
|
State::InProgress => {
|
||||||
|
let elapsed = self.session_start.elapsed();
|
||||||
|
self.session_remaining = self.user_duration.saturating_sub(elapsed);
|
||||||
|
update_session_stats(self);
|
||||||
|
|
||||||
|
if self.session_remaining.is_zero() {
|
||||||
|
self.to_end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
State::ShouldQuit => {}
|
||||||
|
State::Paused => {
|
||||||
|
window::minimize_other(&constants::APP_TITLE);
|
||||||
update_session_stats(self);
|
update_session_stats(self);
|
||||||
}
|
}
|
||||||
State::ShouldQuit => {},
|
State::End => {
|
||||||
|
window::minimize_other(&constants::APP_TITLE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.last_tick_50ms = Instant::now();
|
self.last_tick_50ms = Instant::now();
|
||||||
@@ -92,11 +234,60 @@ 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 {
|
||||||
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<'_>> {
|
||||||
|
self.session_results
|
||||||
|
.iter()
|
||||||
|
.map(|r| {
|
||||||
|
Line::from(Span::styled(
|
||||||
|
format!("{} {}", duration_as_str(&r.duration), r.intention,),
|
||||||
|
match r.rating {
|
||||||
|
2 => Style::new().fg(Color::LightYellow),
|
||||||
|
3 => Style::new().fg(Color::LightGreen),
|
||||||
|
_ => Style::new().fg(Color::LightRed),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_session_stats(&self) -> Vec<Line<'_>> {
|
||||||
|
let mut zero_encountered = if self.state != State::End {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
self.session_ratings
|
||||||
|
.iter()
|
||||||
|
.map(|s| {
|
||||||
|
Line::from(Span::styled(
|
||||||
|
format!("{}: {}", duration_as_str(&s.duration), s.window_title),
|
||||||
|
match s.rating {
|
||||||
|
0 if !zero_encountered => {
|
||||||
|
zero_encountered = true;
|
||||||
|
Style::new().fg(Color::LightBlue)
|
||||||
|
}
|
||||||
|
0 if zero_encountered => Style::new(),
|
||||||
|
1 => Style::new().fg(Color::LightRed),
|
||||||
|
2 => Style::new().fg(Color::LightYellow),
|
||||||
|
3 => Style::new().fg(Color::LightGreen),
|
||||||
|
_ => Style::new().fg(Color::LightBlue),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn duration_as_str(duration: &Duration) -> String {
|
fn duration_as_str(duration: &Duration) -> String {
|
||||||
@@ -107,27 +298,24 @@ fn duration_as_str(duration: &Duration) -> String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn session_stats_to_lines(session_stats: &HashMap<Rc<String>, Duration>) -> Vec<Line> {
|
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()
|
||||||
.map(|(title, duration)| (title.clone(), *duration))
|
.filter(|(_, duration)| duration.as_secs() > 30)
|
||||||
.collect();
|
.map(|(title, duration)| SessionRating {
|
||||||
stats.sort_by(|a, b| b.1.cmp(&a.1));
|
window_title: title.clone(),
|
||||||
stats
|
duration: duration.clone(),
|
||||||
.iter()
|
rating: 0,
|
||||||
.map(|(title, duration)| {
|
|
||||||
Line::from(Span::raw(format!(
|
|
||||||
"{}: {}",
|
|
||||||
duration_as_str(&duration),
|
|
||||||
title
|
|
||||||
)))
|
|
||||||
})
|
})
|
||||||
.collect()
|
.collect();
|
||||||
|
stats.sort_by(|a, b| b.duration.cmp(&a.duration));
|
||||||
|
stats
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
stdout().execute(EnterAlternateScreen)?;
|
stdout().execute(EnterAlternateScreen)?;
|
||||||
|
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();
|
||||||
|
|
||||||
@@ -137,6 +325,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(())
|
||||||
@@ -159,9 +348,10 @@ 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::Backspace => {
|
KeyCode::Backspace => {
|
||||||
let _ = app.user_intention.pop();
|
let _ = app.user_intention.pop();
|
||||||
}
|
}
|
||||||
@@ -169,10 +359,10 @@ 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::Backspace => {
|
KeyCode::Backspace => {
|
||||||
let _ = app.user_duration_str.pop();
|
let _ = app.user_duration_str.pop();
|
||||||
}
|
}
|
||||||
@@ -180,6 +370,54 @@ fn handle_events(app: &mut App) -> Result<()> {
|
|||||||
app.user_duration_str.push(c);
|
app.user_duration_str.push(c);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
},
|
||||||
|
State::InProgress => match key.code {
|
||||||
|
KeyCode::Char('q') => {
|
||||||
|
app.to_end();
|
||||||
|
}
|
||||||
|
KeyCode::Char('p') => {
|
||||||
|
app.state = State::Paused;
|
||||||
|
}
|
||||||
|
KeyCode::Char('a') => {
|
||||||
|
app.user_duration = app.user_duration.saturating_add(Duration::from_secs(60));
|
||||||
|
}
|
||||||
|
KeyCode::Char('x') => {
|
||||||
|
app.user_duration = app.user_duration.saturating_sub(Duration::from_secs(60));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
State::Paused => {
|
||||||
|
if key.code == KeyCode::Char('p') {
|
||||||
|
app.state = State::InProgress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
State::ShouldQuit => (),
|
||||||
|
State::End => {
|
||||||
|
let code = match key.code {
|
||||||
|
KeyCode::Char('1') => 1,
|
||||||
|
KeyCode::Char('2') => 2,
|
||||||
|
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();
|
||||||
|
save_session(&session_result);
|
||||||
|
app.session_results.push(session_result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +425,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
|
||||||
@@ -197,6 +440,7 @@ fn update_session_stats(app: &mut App) {
|
|||||||
*entry += app.current_window_time.elapsed();
|
*entry += app.current_window_time.elapsed();
|
||||||
app.current_window_time = Instant::now();
|
app.current_window_time = Instant::now();
|
||||||
app.current_window_title = window_title;
|
app.current_window_title = window_title;
|
||||||
|
app.session_ratings = session_stats_as_vec(&app.session_stats);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,31 +449,112 @@ fn ui(frame: &mut Frame, app: &App) {
|
|||||||
Constraint::Min(3),
|
Constraint::Min(3),
|
||||||
Constraint::Min(3),
|
Constraint::Min(3),
|
||||||
Constraint::Percentage(100),
|
Constraint::Percentage(100),
|
||||||
|
Constraint::Min(3),
|
||||||
]);
|
]);
|
||||||
let [layout_intention, layout_countdown, layout_titles] = layout.areas(frame.size());
|
let [layout_intention, layout_duration, layout_titles, layout_status] =
|
||||||
|
layout.areas(frame.size());
|
||||||
|
|
||||||
let input: Vec<Line> = vec![Line::from(Span::raw(&app.user_intention))];
|
let border_type_intention = if app.state == State::InputIntention {
|
||||||
|
BorderType::Thick
|
||||||
|
} else {
|
||||||
|
BorderType::Plain
|
||||||
|
};
|
||||||
|
let input_intention = Line::from(Span::raw(&app.user_intention));
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(input).block(Block::bordered().title("Intention")),
|
Paragraph::new(input_intention).block(
|
||||||
|
Block::bordered()
|
||||||
|
.border_type(border_type_intention)
|
||||||
|
.title(constants::INTENTION_TITLE),
|
||||||
|
),
|
||||||
layout_intention,
|
layout_intention,
|
||||||
);
|
);
|
||||||
|
|
||||||
let input: Vec<Line> = if app.state == State::InProgress {
|
let input_duration: Vec<Line> = match app.state {
|
||||||
let remaining = app.user_duration.saturating_sub(app.session_start.elapsed());
|
State::InProgress | State::Paused => {
|
||||||
let s = duration_as_str(&remaining);
|
let s = duration_as_str(&app.session_remaining);
|
||||||
vec![Line::from(Span::raw(s))]
|
vec![Line::from(Span::raw(s))]
|
||||||
} else {
|
}
|
||||||
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 {
|
||||||
|
BorderType::Thick
|
||||||
|
} else {
|
||||||
|
BorderType::Plain
|
||||||
};
|
};
|
||||||
|
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(input).block(Block::bordered().title("Duration")),
|
Paragraph::new(input_duration).block(
|
||||||
layout_countdown,
|
Block::bordered()
|
||||||
|
.border_type(border_type_duration)
|
||||||
|
.title(constants::DURATION_TITLE),
|
||||||
|
),
|
||||||
|
layout_duration,
|
||||||
);
|
);
|
||||||
|
|
||||||
let stats = session_stats_to_lines(&app.session_stats);
|
if app.state == State::InputIntention || app.state == State::InputDuration {
|
||||||
|
let results = app.get_session_results();
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(results)
|
||||||
|
.block(Block::bordered().title(constants::PREVIOUS_SESSIONS_TITLE)),
|
||||||
|
layout_titles,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let stats = app.get_session_stats();
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(stats).block(Block::bordered().title(constants::SESSION_STATS_TITLE)),
|
||||||
|
layout_titles,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut spans: Vec<Span> = Vec::new();
|
||||||
|
if app.user_intention.len() == 0 {
|
||||||
|
let span = Span::styled(
|
||||||
|
constants::PROVIDE_INTENTION,
|
||||||
|
Style::new().fg(Color::LightRed),
|
||||||
|
);
|
||||||
|
spans.push(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.user_duration.is_zero() {
|
||||||
|
let span = Span::styled(
|
||||||
|
constants::PROVIDE_VALID_DURATION,
|
||||||
|
Style::new().fg(Color::LightRed),
|
||||||
|
);
|
||||||
|
spans.push(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
match app.state {
|
||||||
|
State::InputIntention | State::InputDuration => {
|
||||||
|
if spans.len() == 0 {
|
||||||
|
let span = Span::styled(
|
||||||
|
constants::READY_TO_START,
|
||||||
|
Style::new().fg(Color::LightGreen),
|
||||||
|
);
|
||||||
|
spans.push(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
State::InProgress => {
|
||||||
|
let span = Span::styled(
|
||||||
|
constants::SESSION_IN_PROGRESS,
|
||||||
|
Style::new().fg(Color::LightGreen),
|
||||||
|
);
|
||||||
|
spans.push(span);
|
||||||
|
}
|
||||||
|
State::Paused => {
|
||||||
|
let span = Span::styled(constants::SESSION_PAUSED, Style::new().fg(Color::LightBlue));
|
||||||
|
spans.push(span);
|
||||||
|
}
|
||||||
|
State::ShouldQuit => {}
|
||||||
|
State::End => {
|
||||||
|
let span = Span::styled(constants::RATE_TITLES, Style::new().fg(Color::LightBlue));
|
||||||
|
spans.push(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let input_status: Vec<Line> = vec![Line::from(spans)];
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(stats).block(Block::bordered().title("Session")),
|
Paragraph::new(input_status).block(Block::bordered().title(constants::STATUS_TITLE)),
|
||||||
layout_titles,
|
layout_status,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ pub fn get_title_clean() -> String {
|
|||||||
re.replace_all(&title, "").to_string()
|
re.replace_all(&title, "").to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn minimize_other(class: &str) {
|
pub fn minimize_other(title: &str) {
|
||||||
let window_info = get_window_info();
|
let window_info = get_window_info();
|
||||||
if &window_info.class != class {
|
if &window_info.title != title {
|
||||||
run(&format!("xdotool windowminimize {}", window_info.wid));
|
run(&format!("xdotool windowminimize {}", window_info.wid));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WindowInfo {
|
struct WindowInfo {
|
||||||
title: String,
|
title: String,
|
||||||
class: String,
|
_class: String,
|
||||||
wid: String,
|
wid: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ fn run(cmd: &str) -> Option<String> {
|
|||||||
fn get_window_info() -> WindowInfo {
|
fn get_window_info() -> WindowInfo {
|
||||||
let none = WindowInfo {
|
let none = WindowInfo {
|
||||||
title: "none".to_string(),
|
title: "none".to_string(),
|
||||||
class: "none".to_string(),
|
_class: "none".to_string(),
|
||||||
wid: "".to_string(),
|
wid: "".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,5 +62,9 @@ fn get_window_info() -> WindowInfo {
|
|||||||
return none;
|
return none;
|
||||||
};
|
};
|
||||||
|
|
||||||
WindowInfo { title, class, wid }
|
WindowInfo {
|
||||||
|
title,
|
||||||
|
_class: class,
|
||||||
|
wid,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
9
src/window/mod.rs
Normal file
9
src/window/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
mod windows;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub use windows::*;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
mod linux;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub use linux::*;
|
||||||
36
src/window/windows.rs
Normal file
36
src/window/windows.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use regex::Regex;
|
||||||
|
use std::{ffi::OsString, os::windows::ffi::OsStringExt};
|
||||||
|
use winapi::shared::windef::HWND;
|
||||||
|
use winapi::um::winuser::{GetForegroundWindow, GetWindowTextW, ShowWindow, SW_MINIMIZE};
|
||||||
|
|
||||||
|
pub fn get_title_clean() -> String {
|
||||||
|
let title = get_window_info().title;
|
||||||
|
let re = Regex::new(r"-?\d+([:.]\d+)+%?").unwrap();
|
||||||
|
re.replace_all(&title, "").to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn minimize_other(title: &str) {
|
||||||
|
let window_info = get_window_info();
|
||||||
|
if window_info.title != title {
|
||||||
|
unsafe {
|
||||||
|
ShowWindow(window_info.hwnd, SW_MINIMIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WindowInfo {
|
||||||
|
title: String,
|
||||||
|
hwnd: HWND,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_window_info() -> WindowInfo {
|
||||||
|
unsafe {
|
||||||
|
let hwnd = GetForegroundWindow();
|
||||||
|
let mut text: [u16; 512] = [0; 512];
|
||||||
|
let len = GetWindowTextW(hwnd, text.as_mut_ptr(), text.len() as i32) as usize;
|
||||||
|
let title = OsString::from_wide(&text[..len])
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned();
|
||||||
|
WindowInfo { title, hwnd }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user