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:
Jens Krause
2025-01-28 19:28:34 +01:00
committed by GitHub
parent 97787f718d
commit d3c436da0b
14 changed files with 957 additions and 221 deletions

View File

@@ -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();
}
}

View File

@@ -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,

View File

@@ -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),

View File

@@ -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}");
}
}),
},
}

View File

@@ -13,7 +13,6 @@ use ratatui::{
};
use std::cmp::max;
#[derive(Debug, Clone)]
pub struct TimerState {
clock: ClockState<clock::Timer>,
}