feat: native desktop notifications (experimental) (#59)
* desktop notification by entering `Mode::DONE` for `countdown` and `pomodoro` * remove redundant `on_done_called` check * remove build warning (release only) * log notification errors * cli arg to enable desktop notifications * persistant notification settings * ctrl shortcut * update changelog * max timer notification
This commit is contained in:
@@ -65,7 +65,6 @@ pub enum Format {
|
||||
HhMmSs,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClockState<T> {
|
||||
initial_value: DurationEx,
|
||||
current_value: DurationEx,
|
||||
@@ -73,6 +72,7 @@ pub struct ClockState<T> {
|
||||
mode: Mode,
|
||||
format: Format,
|
||||
pub with_decis: bool,
|
||||
on_done: Option<Box<dyn Fn() + 'static>>,
|
||||
phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
@@ -310,6 +310,28 @@ impl<T> ClockState<T> {
|
||||
self.mode == Mode::Done
|
||||
}
|
||||
|
||||
pub fn with_on_done_by_condition(
|
||||
mut self,
|
||||
condition: bool,
|
||||
handler: impl Fn() + 'static,
|
||||
) -> Self {
|
||||
if condition {
|
||||
self.on_done = Some(Box::new(handler));
|
||||
self
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn done(&mut self) {
|
||||
if !self.is_done() {
|
||||
self.mode = Mode::Done;
|
||||
if let Some(handler) = &mut self.on_done {
|
||||
handler();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn update_format(&mut self) {
|
||||
self.format = self.get_format();
|
||||
}
|
||||
@@ -355,6 +377,7 @@ impl ClockState<Countdown> {
|
||||
},
|
||||
format: Format::S,
|
||||
with_decis,
|
||||
on_done: None,
|
||||
phantom: PhantomData,
|
||||
};
|
||||
// update format once
|
||||
@@ -365,14 +388,14 @@ impl ClockState<Countdown> {
|
||||
pub fn tick(&mut self) {
|
||||
if self.mode == Mode::Tick {
|
||||
self.current_value = self.current_value.saturating_sub(self.tick_value);
|
||||
self.set_done();
|
||||
self.check_done();
|
||||
self.update_format();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_done(&mut self) {
|
||||
fn check_done(&mut self) {
|
||||
if self.current_value.eq(&Duration::ZERO.into()) {
|
||||
self.mode = Mode::Done;
|
||||
self.done();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,8 +445,9 @@ impl ClockState<Timer> {
|
||||
Mode::Pause
|
||||
},
|
||||
format: Format::S,
|
||||
phantom: PhantomData,
|
||||
with_decis,
|
||||
on_done: None,
|
||||
phantom: PhantomData,
|
||||
};
|
||||
// update format once
|
||||
instance.update_format();
|
||||
@@ -433,14 +457,14 @@ impl ClockState<Timer> {
|
||||
pub fn tick(&mut self) {
|
||||
if self.mode == Mode::Tick {
|
||||
self.current_value = self.current_value.saturating_add(self.tick_value);
|
||||
self.set_done();
|
||||
self.check_done();
|
||||
self.update_format();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_done(&mut self) {
|
||||
fn check_done(&mut self) {
|
||||
if self.current_value.ge(&MAX_DURATION.into()) {
|
||||
self.mode = Mode::Done;
|
||||
self.done();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ use crate::{
|
||||
edit_time::EditTimeState,
|
||||
},
|
||||
};
|
||||
|
||||
use crossterm::event::KeyModifiers;
|
||||
use notify_rust::Notification;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
crossterm::event::KeyCode,
|
||||
@@ -17,6 +19,7 @@ use ratatui::{
|
||||
text::Line,
|
||||
widgets::{StatefulWidget, Widget},
|
||||
};
|
||||
use tracing::{debug, error};
|
||||
|
||||
use std::ops::Sub;
|
||||
use std::{cmp::max, time::Duration};
|
||||
@@ -24,8 +27,16 @@ use time::OffsetDateTime;
|
||||
|
||||
use super::edit_time::{EditTimeStateArgs, EditTimeWidget};
|
||||
|
||||
pub struct CountdownStateArgs {
|
||||
pub initial_value: Duration,
|
||||
pub current_value: Duration,
|
||||
pub elapsed_value: Duration,
|
||||
pub app_time: AppTime,
|
||||
pub with_decis: bool,
|
||||
pub with_notification: bool,
|
||||
}
|
||||
|
||||
/// State for Countdown Widget
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CountdownState {
|
||||
/// clock to count down
|
||||
clock: ClockState<clock::Countdown>,
|
||||
@@ -37,13 +48,32 @@ pub struct CountdownState {
|
||||
}
|
||||
|
||||
impl CountdownState {
|
||||
pub fn new(
|
||||
clock: ClockState<clock::Countdown>,
|
||||
elapsed_value: Duration,
|
||||
app_time: AppTime,
|
||||
) -> Self {
|
||||
pub fn new(args: CountdownStateArgs) -> Self {
|
||||
let CountdownStateArgs {
|
||||
initial_value,
|
||||
current_value,
|
||||
elapsed_value,
|
||||
with_notification,
|
||||
with_decis,
|
||||
app_time,
|
||||
} = args;
|
||||
|
||||
Self {
|
||||
clock,
|
||||
clock: ClockState::<clock::Countdown>::new(ClockStateArgs {
|
||||
initial_value,
|
||||
current_value,
|
||||
tick_value: Duration::from_millis(TICK_VALUE_MS),
|
||||
with_decis,
|
||||
})
|
||||
.with_on_done_by_condition(with_notification, || {
|
||||
debug!("on_done COUNTDOWN");
|
||||
let result = Notification::new()
|
||||
.summary(&"Countdown done!".to_uppercase())
|
||||
.show();
|
||||
if let Err(err) = result {
|
||||
error!("on_done COUNTDOWN error: {err}");
|
||||
}
|
||||
}),
|
||||
elapsed_clock: ClockState::<clock::Timer>::new(ClockStateArgs {
|
||||
initial_value: Duration::ZERO,
|
||||
current_value: elapsed_value,
|
||||
|
||||
@@ -155,7 +155,7 @@ impl StatefulWidget for Footer {
|
||||
if self.selected_content == Content::Countdown {
|
||||
spans.extend_from_slice(&[
|
||||
Span::from(SPACE),
|
||||
Span::from("[ctrl+e]dit by local time"),
|
||||
Span::from("[^e]dit by local time"),
|
||||
]);
|
||||
}
|
||||
if self.selected_content == Content::Pomodoro {
|
||||
@@ -169,7 +169,7 @@ impl StatefulWidget for Footer {
|
||||
others => vec![
|
||||
Span::from(match others {
|
||||
AppEditMode::Clock => "[e]dit done",
|
||||
AppEditMode::Time => "[ctrl+e]dit done",
|
||||
AppEditMode::Time => "[^e]dit done",
|
||||
_ => "",
|
||||
}),
|
||||
Span::from(SPACE),
|
||||
|
||||
@@ -3,8 +3,9 @@ use crate::{
|
||||
constants::TICK_VALUE_MS,
|
||||
events::{Event, EventHandler},
|
||||
utils::center,
|
||||
widgets::clock::{ClockState, ClockWidget, Countdown},
|
||||
widgets::clock::{ClockState, ClockStateArgs, ClockWidget, Countdown},
|
||||
};
|
||||
use notify_rust::Notification;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
crossterm::event::KeyCode,
|
||||
@@ -12,13 +13,10 @@ use ratatui::{
|
||||
text::Line,
|
||||
widgets::{StatefulWidget, Widget},
|
||||
};
|
||||
use std::{cmp::max, time::Duration};
|
||||
|
||||
use strum::Display;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::clock::ClockStateArgs;
|
||||
use std::{cmp::max, time::Duration};
|
||||
use strum::Display;
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[derive(Debug, Clone, Display, Hash, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub enum Mode {
|
||||
@@ -26,7 +24,6 @@ pub enum Mode {
|
||||
Pause,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClockMap {
|
||||
work: ClockState<Countdown>,
|
||||
pause: ClockState<Countdown>,
|
||||
@@ -47,7 +44,6 @@ impl ClockMap {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PomodoroState {
|
||||
mode: Mode,
|
||||
clock_map: ClockMap,
|
||||
@@ -60,6 +56,7 @@ pub struct PomodoroStateArgs {
|
||||
pub initial_value_pause: Duration,
|
||||
pub current_value_pause: Duration,
|
||||
pub with_decis: bool,
|
||||
pub with_notification: bool,
|
||||
}
|
||||
|
||||
impl PomodoroState {
|
||||
@@ -71,6 +68,7 @@ impl PomodoroState {
|
||||
initial_value_pause,
|
||||
current_value_pause,
|
||||
with_decis,
|
||||
with_notification,
|
||||
} = args;
|
||||
Self {
|
||||
mode,
|
||||
@@ -80,12 +78,30 @@ impl PomodoroState {
|
||||
current_value: current_value_work,
|
||||
tick_value: Duration::from_millis(TICK_VALUE_MS),
|
||||
with_decis,
|
||||
})
|
||||
.with_on_done_by_condition(with_notification, || {
|
||||
debug!("on_done WORK");
|
||||
let result = Notification::new()
|
||||
.summary(&"Work done!".to_uppercase())
|
||||
.show();
|
||||
if let Err(err) = result {
|
||||
error!("on_done WORK error: {err}");
|
||||
}
|
||||
}),
|
||||
pause: ClockState::<Countdown>::new(ClockStateArgs {
|
||||
initial_value: initial_value_pause,
|
||||
current_value: current_value_pause,
|
||||
tick_value: Duration::from_millis(TICK_VALUE_MS),
|
||||
with_decis,
|
||||
})
|
||||
.with_on_done_by_condition(with_notification, || {
|
||||
debug!("on_done PAUSE");
|
||||
let result = Notification::new()
|
||||
.summary(&"Pause done!".to_uppercase())
|
||||
.show();
|
||||
if let Err(err) = result {
|
||||
error!("on_done PAUSE error: {err}");
|
||||
}
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ use ratatui::{
|
||||
};
|
||||
use std::cmp::max;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TimerState {
|
||||
clock: ClockState<clock::Timer>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user