initial commit

This commit is contained in:
Dr. Matthias Ratajczak
2023-03-01 17:47:33 +01:00
commit 1136617443
4 changed files with 682 additions and 0 deletions
+196
View File
@@ -0,0 +1,196 @@
use std::io::{stdout, Stdout, Write};
use std::process::Command;
use std::time::Duration;
use crossterm::cursor;
use crossterm::{
cursor::MoveTo,
event::{Event, EventStream, KeyCode, KeyModifiers},
terminal::{self, disable_raw_mode, enable_raw_mode},
ExecutableCommand, Result,
};
use futures::StreamExt;
use tokio::time::sleep;
const ONE_SECOND: Duration = Duration::from_secs(1);
#[tokio::main(flavor = "current_thread")]
async fn main() {
let timer = Pomodoro::new();
let timer_task = tokio::spawn(async move { timer.main_loop().await });
let ui_task = tokio::spawn(async move { Ui::main_loop().await });
tokio::select! (
_ = timer_task => (),
_ = ui_task => (),
);
Ui::clear_terminal().unwrap();
}
struct Ui;
impl Ui {
async fn main_loop() {
let mut reader = EventStream::new();
loop {
enable_raw_mode().unwrap();
stdout().execute(cursor::Hide).unwrap();
if let Some(Ok(Event::Key(key_event))) = reader.next().await {
if Self::is_char_q(key_event) || Self::is_ctrl_c(&key_event) {
disable_raw_mode().unwrap();
stdout().execute(cursor::Show).unwrap();
return;
}
}
}
}
fn is_char_q(key_event: crossterm::event::KeyEvent) -> bool {
key_event.code == KeyCode::Char('q')
}
fn is_ctrl_c(key_event: &crossterm::event::KeyEvent) -> bool {
key_event.code == KeyCode::Char('c') && key_event.modifiers.contains(KeyModifiers::CONTROL)
}
fn clear_terminal() -> Result<()> {
let mut stdout = stdout();
stdout.execute(terminal::Clear(terminal::ClearType::All))?;
stdout.execute(terminal::Clear(terminal::ClearType::Purge))?;
stdout.execute(MoveTo(0, 0))?;
Ok(())
}
}
struct Pomodoro {
interval_idx: usize,
intervals: [Interval; 8],
remaining: Duration,
stdout: Stdout,
}
impl Pomodoro {
fn new() -> Self {
let intervals = Interval::default_sequence();
let interval_idx = 0;
let remaining = intervals[interval_idx].get_duration();
Pomodoro {
interval_idx,
intervals,
remaining,
stdout: stdout(),
}
}
fn show_status(&mut self) -> Result<()> {
Ui::clear_terminal()?;
write!(
self.stdout,
"currently: {} ({}/{})",
self.intervals[self.interval_idx].get_description(),
self.interval_idx + 1,
self.intervals.len()
)?;
self.stdout.execute(MoveTo(0, 1))?;
write!(self.stdout, "remaining: {}", format(self.remaining))?;
self.stdout.flush()?;
Ok(())
}
async fn main_loop(mut self) -> Result<()> {
loop {
'inner: loop {
self.show_status()?;
sleep(ONE_SECOND).await;
self.remaining -= ONE_SECOND;
if self.remaining.is_zero() {
self.show_status()?;
break 'inner;
}
}
self.next_interval();
self.remaining = self.current_interval().get_duration();
self.send_notification(self.current_interval().get_message());
}
}
fn next_interval(&mut self) {
self.interval_idx += 1;
self.interval_idx %= self.intervals.len();
}
fn current_interval(&self) -> Interval {
self.intervals[self.interval_idx]
}
fn send_notification(&self, message: &str) {
let mut command = Command::new("notify-send");
command.args(["-t", "0", message]);
command.status().unwrap();
}
}
#[derive(Debug, Clone, Copy)]
enum Interval {
Work(Duration),
ShortBreak(Duration),
LongBreak(Duration),
}
impl Interval {
const FIVE_MINUTES: Duration = Duration::from_secs(5 * 60);
const FIFTEEN_MINUTES: Duration = Duration::from_secs(15 * 60);
const TWENTY_FIVE_MINUTES: Duration = Duration::from_secs(25 * 60);
fn default_sequence() -> [Self; 8] {
[
Self::Work(Self::TWENTY_FIVE_MINUTES),
Self::ShortBreak(Self::FIVE_MINUTES),
Self::Work(Self::TWENTY_FIVE_MINUTES),
Self::ShortBreak(Self::FIVE_MINUTES),
Self::Work(Self::TWENTY_FIVE_MINUTES),
Self::ShortBreak(Self::FIVE_MINUTES),
Self::Work(Self::TWENTY_FIVE_MINUTES),
Self::LongBreak(Self::FIFTEEN_MINUTES),
]
}
fn get_duration(&self) -> Duration {
match self {
Interval::Work(d) => *d,
Interval::ShortBreak(d) => *d,
Interval::LongBreak(d) => *d,
}
}
fn get_description(&self) -> &str {
match self {
Interval::Work(_) => "work",
Interval::ShortBreak(_) => "short break",
Interval::LongBreak(_) => "long break",
}
}
fn get_message(&self) -> &str {
match self {
Interval::Work(_) => "back to work",
Interval::ShortBreak(_) => "take a break",
Interval::LongBreak(_) => "big break, you earned it!",
}
}
}
fn format(dur: Duration) -> String {
let min = dur.as_secs() / 60;
let sec = dur.as_secs() % 60;
format!("{min}:{sec:0>2}")
}