This commit is contained in:
Dr. Matthias Ratajczak
2023-03-02 16:49:38 +01:00
parent 7bf31b9ee2
commit 37eba7cfa3
6 changed files with 340 additions and 317 deletions
+15 -317
View File
@@ -1,60 +1,46 @@
use std::ffi::OsString;
use std::fs::{self, read_to_string};
use std::io::{stdout, Stdout, Write};
use std::os::unix::prelude::OsStringExt;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
use std::time::Duration;
mod interval;
mod message;
mod pid;
mod pomodoro;
mod ui;
use anyhow::{anyhow, bail, Result};
use std::process::ExitCode;
use crossterm::{cursor, QueueableCommand};
use crossterm::{
cursor::MoveTo,
event::{Event, EventStream, KeyCode, KeyModifiers},
terminal::{self, disable_raw_mode, enable_raw_mode},
ExecutableCommand,
};
use futures::StreamExt;
use tokio::sync::broadcast::{channel, Receiver, Sender};
use anyhow::{bail, Result};
use tokio::sync::broadcast::channel;
use tokio::task::JoinSet;
use tokio::time::sleep;
const ONE_SECOND: Duration = Duration::from_secs(1);
const PID_FILE: &str = "/tmp/pomodoro.pid";
#[tokio::main(flavor = "current_thread")]
async fn main() -> ExitCode {
match main2().await {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("{}", e.root_cause().to_string());
eprintln!("{}", e.root_cause());
ExitCode::FAILURE
}
}
}
async fn main2() -> Result<()> {
let pid = if let Some(pid) = Pid::check_already_running() {
let pid = if let Some(pid) = pid::Pid::check_already_running() {
bail!("already running (pid {})", pid.pid)
} else {
Pid::create()?
pid::Pid::create()?
};
let result = run_all_tasks().await;
Ui::clear_terminal()?;
ui::clear_terminal()?;
pid.remove()?;
result
}
async fn run_all_tasks() -> Result<()> {
let (tx, rx) = channel::<Message>(10);
let (tx, rx) = channel::<message::Message>(10);
let timer = Pomodoro::new(rx);
let ui = Ui::new(tx);
let timer = pomodoro::Pomodoro::new(rx);
let ui = ui::Ui::new(tx);
let mut tasks = JoinSet::new();
@@ -67,291 +53,3 @@ async fn run_all_tasks() -> Result<()> {
Ok(())
}
struct Pid {
pid: u32,
path: PathBuf,
}
impl Pid {
fn check_already_running() -> Option<Self> {
let path = PathBuf::from(PID_FILE);
Self::get_pid_from_file(&path)
.and_then(Self::check_process_exists)
.map(|pid| Self { pid, path })
}
fn check_process_exists(pid: u32) -> Option<u32> {
let mut path = PathBuf::from("/proc");
path.push(pid.to_string());
path.push("cmdline");
let content = fs::read(path).ok()?;
let cmdline = OsString::from_vec(content).to_string_lossy().to_string();
if cmdline.contains("pomodoro") {
Some(pid)
} else {
None
}
}
fn get_pid_from_file(file: &Path) -> Option<u32> {
let content = read_to_string(file).ok()?;
let line = content.lines().nth(0)?;
let result: u32 = line.parse().ok()?;
Some(result)
}
fn create() -> Result<Self> {
let pid = std::process::id();
let path = PathBuf::from(PID_FILE);
let mut fd = fs::File::create(&path)?;
write!(fd, "{pid}")?;
let result = Self { pid, path };
Ok(result)
}
fn remove(self) -> Result<()> {
fs::remove_file(self.path).map_err(|e| anyhow!(e))
}
}
struct Ui {
sender: Sender<Message>,
}
impl Ui {
fn new(sender: Sender<Message>) -> Self {
Self { sender }
}
async fn main_loop(self) -> Result<()> {
let mut reader = EventStream::new();
loop {
enable_raw_mode()?;
stdout().execute(cursor::Hide)?;
if let Some(Ok(Event::Key(key_event))) = reader.next().await {
let should_exit = self.handle_key_event(key_event)?;
if should_exit {
return Ok(());
};
}
}
}
fn handle_key_event(&self, key_event: crossterm::event::KeyEvent) -> Result<bool> {
if Self::is_char_q(key_event) {
self.prepare_for_program_quit()?;
return Ok(true);
};
if Self::is_ctrl_c(key_event) {
self.prepare_for_program_quit()?;
self.sender.send(Message::CtrlC)?;
bail!("<Ctrl-C>");
}
if Self::is_space(key_event) {
self.sender.send(Message::TogglePause)?;
}
Ok(false)
}
fn prepare_for_program_quit(&self) -> Result<()> {
self.sender.send(Message::Quit)?;
disable_raw_mode()?;
stdout().execute(cursor::Show)?;
Ok(())
}
fn is_char_q(key_event: crossterm::event::KeyEvent) -> bool {
key_event.code == KeyCode::Char('q')
}
fn is_space(key_event: crossterm::event::KeyEvent) -> bool {
key_event.code == KeyCode::Char(' ')
}
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.queue(terminal::Clear(terminal::ClearType::All))?;
stdout.queue(terminal::Clear(terminal::ClearType::Purge))?;
stdout.queue(MoveTo(0, 0))?;
stdout.flush()?;
Ok(())
}
}
struct Pomodoro {
interval_idx: usize,
intervals: [Interval; 8],
remaining: Duration,
stdout: Stdout,
receiver: Receiver<Message>,
paused: bool,
}
impl Pomodoro {
fn new(receiver: Receiver<Message>) -> Self {
let intervals = Interval::default_sequence();
let interval_idx = 0;
let remaining = intervals[interval_idx].get_duration();
Pomodoro {
interval_idx,
intervals,
remaining,
stdout: stdout(),
receiver,
paused: false,
}
}
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.queue(MoveTo(0, 1))?;
write!(self.stdout, "remaining: {}", format(self.remaining))?;
if self.paused {
write!(self.stdout, " (paused)")?;
}
self.stdout.flush()?;
Ok(())
}
fn toggle_pause(&mut self) {
self.paused ^= true;
}
async fn main_loop(mut self) -> Result<()> {
loop {
'inner: loop {
self.show_status()?;
tokio::select! (
_ = sleep(ONE_SECOND) => {
if !self.paused {
self.remaining -= ONE_SECOND;
}
},
message = self.receiver.recv() => {
if let Ok(m) = message{
match m {
Message::Quit => {
return Ok(());
},
Message::CtrlC => {
bail!("<Ctrl-C>");
},
Message::TogglePause => {
self.toggle_pause();
continue;
}
}
} else {
bail!("<terminate>");
}
}
);
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) -> Result<()> {
let mut command = Command::new("notify-send");
command.args(["-t", "0", message]);
command.status()?;
Ok(())
}
}
#[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}")
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Message {
CtrlC,
Quit,
TogglePause,
}