refactor
This commit is contained in:
@@ -0,0 +1,51 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub(crate) 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);
|
||||||
|
|
||||||
|
pub(crate) 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),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_duration(&self) -> Duration {
|
||||||
|
match self {
|
||||||
|
Interval::Work(d) => *d,
|
||||||
|
Interval::ShortBreak(d) => *d,
|
||||||
|
Interval::LongBreak(d) => *d,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_description(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Interval::Work(_) => "work",
|
||||||
|
Interval::ShortBreak(_) => "short break",
|
||||||
|
Interval::LongBreak(_) => "long break",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_message(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Interval::Work(_) => "back to work",
|
||||||
|
Interval::ShortBreak(_) => "take a break",
|
||||||
|
Interval::LongBreak(_) => "big break, you earned it!",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-317
@@ -1,60 +1,46 @@
|
|||||||
use std::ffi::OsString;
|
mod interval;
|
||||||
use std::fs::{self, read_to_string};
|
mod message;
|
||||||
use std::io::{stdout, Stdout, Write};
|
mod pid;
|
||||||
use std::os::unix::prelude::OsStringExt;
|
mod pomodoro;
|
||||||
use std::path::{Path, PathBuf};
|
mod ui;
|
||||||
use std::process::{Command, ExitCode};
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
use std::process::ExitCode;
|
||||||
|
|
||||||
use crossterm::{cursor, QueueableCommand};
|
use anyhow::{bail, Result};
|
||||||
use crossterm::{
|
use tokio::sync::broadcast::channel;
|
||||||
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 tokio::task::JoinSet;
|
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")]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
async fn main() -> ExitCode {
|
async fn main() -> ExitCode {
|
||||||
match main2().await {
|
match main2().await {
|
||||||
Ok(()) => ExitCode::SUCCESS,
|
Ok(()) => ExitCode::SUCCESS,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("{}", e.root_cause().to_string());
|
eprintln!("{}", e.root_cause());
|
||||||
ExitCode::FAILURE
|
ExitCode::FAILURE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn main2() -> Result<()> {
|
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)
|
bail!("already running (pid {})", pid.pid)
|
||||||
} else {
|
} else {
|
||||||
Pid::create()?
|
pid::Pid::create()?
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = run_all_tasks().await;
|
let result = run_all_tasks().await;
|
||||||
|
|
||||||
Ui::clear_terminal()?;
|
ui::clear_terminal()?;
|
||||||
pid.remove()?;
|
pid.remove()?;
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_all_tasks() -> 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 timer = pomodoro::Pomodoro::new(rx);
|
||||||
let ui = Ui::new(tx);
|
let ui = ui::Ui::new(tx);
|
||||||
|
|
||||||
let mut tasks = JoinSet::new();
|
let mut tasks = JoinSet::new();
|
||||||
|
|
||||||
@@ -67,291 +53,3 @@ async fn run_all_tasks() -> Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
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,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub(crate) enum Message {
|
||||||
|
CtrlC,
|
||||||
|
Quit,
|
||||||
|
TogglePause,
|
||||||
|
}
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
use std::ffi::OsString;
|
||||||
|
use std::fs;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::os::unix::ffi::OsStringExt;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
|
||||||
|
const PID_FILE: &str = "/tmp/pomodoro.pid";
|
||||||
|
|
||||||
|
pub(crate) struct Pid {
|
||||||
|
pub(crate) pid: u32,
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pid {
|
||||||
|
pub(crate) fn check_already_running() -> Option<Self> {
|
||||||
|
let path = PathBuf::from(PID_FILE);
|
||||||
|
get_pid_from_file(&path)
|
||||||
|
.and_then(check_process_exists)
|
||||||
|
.map(|pid| Self { pid, path })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remove(self) -> Result<()> {
|
||||||
|
fs::remove_file(self.path).map_err(|e| anyhow!(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_pid_from_file(file: &Path) -> Option<u32> {
|
||||||
|
let content = fs::read_to_string(file).ok()?;
|
||||||
|
let line = content.lines().next()?;
|
||||||
|
let result: u32 = line.parse().ok()?;
|
||||||
|
Some(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
use std::io::{stdout, Stdout, Write};
|
||||||
|
use std::process::Command;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use crossterm::{cursor::MoveTo, QueueableCommand};
|
||||||
|
use tokio::sync::broadcast::Receiver;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
|
use crate::interval::Interval;
|
||||||
|
use crate::message::Message;
|
||||||
|
use crate::ui;
|
||||||
|
|
||||||
|
const ONE_SECOND: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
|
pub(crate) struct Pomodoro {
|
||||||
|
interval_idx: usize,
|
||||||
|
intervals: [Interval; 8],
|
||||||
|
remaining: Duration,
|
||||||
|
stdout: Stdout,
|
||||||
|
receiver: Receiver<Message>,
|
||||||
|
paused: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pomodoro {
|
||||||
|
pub(crate) 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format(dur: Duration) -> String {
|
||||||
|
let min = dur.as_secs() / 60;
|
||||||
|
let sec = dur.as_secs() % 60;
|
||||||
|
|
||||||
|
format!("{min}:{sec:0>2}")
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
use std::io::stdout;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use crossterm::{
|
||||||
|
cursor::{self, MoveTo},
|
||||||
|
event::{Event, EventStream, KeyCode, KeyModifiers},
|
||||||
|
terminal::{self, disable_raw_mode, enable_raw_mode},
|
||||||
|
ExecutableCommand, QueueableCommand,
|
||||||
|
};
|
||||||
|
use futures::StreamExt;
|
||||||
|
use tokio::sync::broadcast::Sender;
|
||||||
|
|
||||||
|
use crate::message::Message;
|
||||||
|
|
||||||
|
pub(crate) struct Ui {
|
||||||
|
sender: Sender<Message>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ui {
|
||||||
|
pub(crate) fn new(sender: Sender<Message>) -> Self {
|
||||||
|
Self { sender }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) 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(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user