fix(notification): remove callbacks in favour of mpsc messaging (#64)

This commit is contained in:
Jens Krause 2025-02-05 13:35:24 +01:00 committed by GitHub
parent 7ff167368d
commit 886deb3311
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 70 additions and 94 deletions

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
args::Args, args::Args,
common::{AppEditMode, AppTime, AppTimeFormat, Content, Notification, Style}, common::{AppEditMode, AppTime, AppTimeFormat, ClockTypeId, Content, Notification, Style},
constants::TICK_VALUE_MS, constants::TICK_VALUE_MS,
events::{self, TuiEventHandler}, events::{self, TuiEventHandler},
storage::AppStorage, storage::AppStorage,
@ -155,7 +155,6 @@ impl App {
elapsed_value: elapsed_value_countdown, elapsed_value: elapsed_value_countdown,
app_time, app_time,
with_decis, with_decis,
with_notification: notification == Notification::On,
app_tx: app_tx.clone(), app_tx: app_tx.clone(),
}), }),
timer: TimerState::new( timer: TimerState::new(
@ -164,20 +163,9 @@ impl App {
current_value: current_value_timer, current_value: current_value_timer,
tick_value: Duration::from_millis(TICK_VALUE_MS), tick_value: Duration::from_millis(TICK_VALUE_MS),
with_decis, with_decis,
app_tx: None, app_tx: Some(app_tx.clone()),
}) })
.with_on_done_by_condition( .with_name("Timer".to_owned()),
notification == Notification::On,
|| {
debug!("on_done TIMER");
let result = notify_rust::Notification::new()
.summary(&"Timer stopped by reaching its maximum value".to_uppercase())
.show();
if let Err(err) = result {
error!("on_done TIMER error: {err}");
}
},
),
), ),
pomodoro: PomodoroState::new(PomodoroStateArgs { pomodoro: PomodoroState::new(PomodoroStateArgs {
mode: pomodoro_mode, mode: pomodoro_mode,
@ -186,7 +174,6 @@ impl App {
initial_value_pause, initial_value_pause,
current_value_pause, current_value_pause,
with_decis, with_decis,
with_notification: notification == Notification::On,
app_tx: app_tx.clone(), app_tx: app_tx.clone(),
}), }),
footer: FooterState::new(show_menu, app_time_format), footer: FooterState::new(show_menu, app_time_format),
@ -253,8 +240,25 @@ impl App {
// Closure to handle `AppEvent`'s // Closure to handle `AppEvent`'s
let handle_app_events = |app: &mut Self, event: events::AppEvent| -> Result<()> { let handle_app_events = |app: &mut Self, event: events::AppEvent| -> Result<()> {
match event { match event {
events::AppEvent::ClockDone => { events::AppEvent::ClockDone(type_id, name) => {
debug!("AppEvent::ClockDone"); debug!("AppEvent::ClockDone");
if app.notification == Notification::On {
let msg = match type_id {
ClockTypeId::Timer => {
format!("{name} stopped by reaching its maximum value.")
}
_ => format!("{:?} {name} done!", type_id),
};
// notification
let result = notify_rust::Notification::new()
.summary(&msg.to_uppercase())
.show();
if let Err(err) = result {
error!("on_done {name} error: {err}");
}
};
#[cfg(feature = "sound")] #[cfg(feature = "sound")]
if let Some(path) = app.sound_path.clone() { if let Some(path) = app.sound_path.clone() {
_ = Sound::new(path).and_then(|sound| sound.play()).or_else( _ = Sound::new(path).and_then(|sound| sound.play()).or_else(

View File

@ -17,6 +17,12 @@ pub enum Content {
Pomodoro, Pomodoro,
} }
#[derive(Clone, Debug)]
pub enum ClockTypeId {
Countdown,
Timer,
}
#[derive(Debug, Copy, Clone, ValueEnum, Default, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, ValueEnum, Default, Serialize, Deserialize)]
pub enum Style { pub enum Style {
#[default] #[default]

View File

@ -5,6 +5,7 @@ use tokio::sync::mpsc;
use tokio::time::interval; use tokio::time::interval;
use tokio_stream::{wrappers::IntervalStream, StreamMap}; use tokio_stream::{wrappers::IntervalStream, StreamMap};
use crate::common::ClockTypeId;
use crate::constants::{FPS_VALUE_MS, TICK_VALUE_MS}; use crate::constants::{FPS_VALUE_MS, TICK_VALUE_MS};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
@ -25,7 +26,7 @@ pub enum TuiEvent {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum AppEvent { pub enum AppEvent {
ClockDone, ClockDone(ClockTypeId, String),
} }
pub type AppEventTx = mpsc::UnboundedSender<AppEvent>; pub type AppEventTx = mpsc::UnboundedSender<AppEvent>;

View File

@ -10,7 +10,7 @@ use ratatui::{
}; };
use crate::{ use crate::{
common::Style, common::{ClockTypeId, Style},
duration::{DurationEx, MAX_DURATION, ONE_DECI_SECOND, ONE_HOUR, ONE_MINUTE, ONE_SECOND}, duration::{DurationEx, MAX_DURATION, ONE_DECI_SECOND, ONE_HOUR, ONE_MINUTE, ONE_SECOND},
events::{AppEvent, AppEventTx}, events::{AppEvent, AppEventTx},
utils::center_horizontal, utils::center_horizontal,
@ -67,13 +67,14 @@ pub enum Format {
} }
pub struct ClockState<T> { pub struct ClockState<T> {
type_id: ClockTypeId,
name: Option<String>,
initial_value: DurationEx, initial_value: DurationEx,
current_value: DurationEx, current_value: DurationEx,
tick_value: DurationEx, tick_value: DurationEx,
mode: Mode, mode: Mode,
format: Format, format: Format,
pub with_decis: bool, pub with_decis: bool,
on_done: Option<Box<dyn Fn() + 'static>>,
app_tx: Option<AppEventTx>, app_tx: Option<AppEventTx>,
phantom: PhantomData<T>, phantom: PhantomData<T>,
} }
@ -87,6 +88,19 @@ pub struct ClockStateArgs {
} }
impl<T> ClockState<T> { impl<T> ClockState<T> {
pub fn with_name(mut self, name: String) -> Self {
self.name = Some(name);
self
}
pub fn get_name(&self) -> String {
self.name.clone().unwrap_or_default()
}
pub fn get_type_id(&self) -> &ClockTypeId {
&self.type_id
}
pub fn with_mode(mut self, mode: Mode) -> Self { pub fn with_mode(mut self, mode: Mode) -> Self {
self.mode = mode; self.mode = mode;
self self
@ -313,27 +327,13 @@ impl<T> ClockState<T> {
self.mode == Mode::Done 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) { fn done(&mut self) {
if !self.is_done() { if !self.is_done() {
self.mode = Mode::Done; self.mode = Mode::Done;
if let Some(handler) = &mut self.on_done { let type_id = self.get_type_id().clone();
handler(); let name = self.get_name();
}; if let Some(tx) = &self.app_tx {
if let Some(tx) = &mut self.app_tx { _ = tx.send(AppEvent::ClockDone(type_id, name));
_ = tx.send(AppEvent::ClockDone);
}; };
} }
} }
@ -372,6 +372,8 @@ impl ClockState<Countdown> {
app_tx, app_tx,
} = args; } = args;
let mut instance = Self { let mut instance = Self {
type_id: ClockTypeId::Countdown,
name: None,
initial_value: initial_value.into(), initial_value: initial_value.into(),
current_value: current_value.into(), current_value: current_value.into(),
tick_value: tick_value.into(), tick_value: tick_value.into(),
@ -384,7 +386,6 @@ impl ClockState<Countdown> {
}, },
format: Format::S, format: Format::S,
with_decis, with_decis,
on_done: None,
app_tx, app_tx,
phantom: PhantomData, phantom: PhantomData,
}; };
@ -443,6 +444,8 @@ impl ClockState<Timer> {
app_tx, app_tx,
} = args; } = args;
let mut instance = Self { let mut instance = Self {
type_id: ClockTypeId::Timer,
name: None,
initial_value: initial_value.into(), initial_value: initial_value.into(),
current_value: current_value.into(), current_value: current_value.into(),
tick_value: tick_value.into(), tick_value: tick_value.into(),
@ -455,7 +458,6 @@ impl ClockState<Timer> {
}, },
format: Format::S, format: Format::S,
with_decis, with_decis,
on_done: None,
app_tx, app_tx,
phantom: PhantomData, phantom: PhantomData,
}; };

View File

@ -1,36 +1,33 @@
use crate::{ use crate::{
common::ClockTypeId,
duration::{ONE_DECI_SECOND, ONE_HOUR, ONE_MINUTE, ONE_SECOND}, duration::{ONE_DECI_SECOND, ONE_HOUR, ONE_MINUTE, ONE_SECOND},
widgets::clock::*, widgets::clock::*,
}; };
use std::time::Duration; use std::time::Duration;
#[test] fn default_args() -> ClockStateArgs {
fn test_toggle_edit() { ClockStateArgs {
let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_HOUR, initial_value: ONE_HOUR,
current_value: ONE_HOUR, current_value: ONE_HOUR,
tick_value: ONE_DECI_SECOND, tick_value: ONE_DECI_SECOND,
with_decis: true, with_decis: false,
app_tx: None, app_tx: None,
}); }
// off by default }
assert!(!c.is_edit_mode());
// toggle on #[test]
c.toggle_edit(); fn test_type_id() {
assert!(c.is_edit_mode()); let c = ClockState::<Timer>::new(default_args());
// toggle off assert!(matches!(c.get_type_id(), ClockTypeId::Timer));
c.toggle_edit(); let c = ClockState::<Countdown>::new(default_args());
assert!(!c.is_edit_mode()); assert!(matches!(c.get_type_id(), ClockTypeId::Countdown));
} }
#[test] #[test]
fn test_default_edit_mode_hhmmss() { fn test_default_edit_mode_hhmmss() {
let mut c = ClockState::<Timer>::new(ClockStateArgs { let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_HOUR,
current_value: ONE_HOUR,
tick_value: ONE_DECI_SECOND,
with_decis: true, with_decis: true,
app_tx: None, ..default_args()
}); });
// toggle on // toggle on

View File

@ -9,9 +9,7 @@ use crate::{
edit_time::{EditTimeState, EditTimeStateArgs, EditTimeWidget}, edit_time::{EditTimeState, EditTimeStateArgs, EditTimeWidget},
}, },
}; };
use crossterm::event::KeyModifiers; use crossterm::event::KeyModifiers;
use notify_rust::Notification;
use ratatui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
crossterm::event::KeyCode, crossterm::event::KeyCode,
@ -19,8 +17,6 @@ use ratatui::{
text::Line, text::Line,
widgets::{StatefulWidget, Widget}, widgets::{StatefulWidget, Widget},
}; };
use tracing::{debug, error};
use std::ops::Sub; use std::ops::Sub;
use std::{cmp::max, time::Duration}; use std::{cmp::max, time::Duration};
use time::OffsetDateTime; use time::OffsetDateTime;
@ -31,7 +27,6 @@ pub struct CountdownStateArgs {
pub elapsed_value: Duration, pub elapsed_value: Duration,
pub app_time: AppTime, pub app_time: AppTime,
pub with_decis: bool, pub with_decis: bool,
pub with_notification: bool,
pub app_tx: AppEventTx, pub app_tx: AppEventTx,
} }
@ -52,7 +47,6 @@ impl CountdownState {
initial_value, initial_value,
current_value, current_value,
elapsed_value, elapsed_value,
with_notification,
with_decis, with_decis,
app_time, app_time,
app_tx, app_tx,
@ -65,15 +59,6 @@ impl CountdownState {
tick_value: Duration::from_millis(TICK_VALUE_MS), tick_value: Duration::from_millis(TICK_VALUE_MS),
with_decis, with_decis,
app_tx: Some(app_tx.clone()), app_tx: Some(app_tx.clone()),
})
.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 { elapsed_clock: ClockState::<clock::Timer>::new(ClockStateArgs {
initial_value: Duration::ZERO, initial_value: Duration::ZERO,
@ -82,6 +67,7 @@ impl CountdownState {
with_decis: false, with_decis: false,
app_tx: None, app_tx: None,
}) })
.with_name("MET".to_owned())
// A previous `elapsed_value > 0` means the `Clock` was running before, // A previous `elapsed_value > 0` means the `Clock` was running before,
// but not in `Initial` state anymore. Updating `Mode` here // but not in `Initial` state anymore. Updating `Mode` here
// is needed to handle `Event::Tick` in `EventHandler::update` properly // is needed to handle `Event::Tick` in `EventHandler::update` properly

View File

@ -5,7 +5,6 @@ use crate::{
utils::center, utils::center,
widgets::clock::{ClockState, ClockStateArgs, ClockWidget, Countdown}, widgets::clock::{ClockState, ClockStateArgs, ClockWidget, Countdown},
}; };
use notify_rust::Notification;
use ratatui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
crossterm::event::KeyCode, crossterm::event::KeyCode,
@ -16,7 +15,6 @@ use ratatui::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{cmp::max, time::Duration}; use std::{cmp::max, time::Duration};
use strum::Display; use strum::Display;
use tracing::{debug, error};
#[derive(Debug, Clone, Display, Hash, Eq, PartialEq, Deserialize, Serialize)] #[derive(Debug, Clone, Display, Hash, Eq, PartialEq, Deserialize, Serialize)]
pub enum Mode { pub enum Mode {
@ -56,7 +54,6 @@ pub struct PomodoroStateArgs {
pub initial_value_pause: Duration, pub initial_value_pause: Duration,
pub current_value_pause: Duration, pub current_value_pause: Duration,
pub with_decis: bool, pub with_decis: bool,
pub with_notification: bool,
pub app_tx: AppEventTx, pub app_tx: AppEventTx,
} }
@ -69,7 +66,6 @@ impl PomodoroState {
initial_value_pause, initial_value_pause,
current_value_pause, current_value_pause,
with_decis, with_decis,
with_notification,
app_tx, app_tx,
} = args; } = args;
Self { Self {
@ -82,15 +78,7 @@ impl PomodoroState {
with_decis, with_decis,
app_tx: Some(app_tx.clone()), app_tx: Some(app_tx.clone()),
}) })
.with_on_done_by_condition(with_notification, || { .with_name("Work".to_owned()),
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 { pause: ClockState::<Countdown>::new(ClockStateArgs {
initial_value: initial_value_pause, initial_value: initial_value_pause,
current_value: current_value_pause, current_value: current_value_pause,
@ -98,15 +86,7 @@ impl PomodoroState {
with_decis, with_decis,
app_tx: Some(app_tx), app_tx: Some(app_tx),
}) })
.with_on_done_by_condition(with_notification, || { .with_name("Pause".to_owned()),
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}");
}
}),
}, },
} }
} }