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:
parent
97787f718d
commit
d3c436da0b
@ -1,5 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.1.0 - 2025-__-__
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- (notification) Native desktop notifications (experimental) [#49](https://github.com/sectore/timr-tui/pull/59)
|
||||||
|
|
||||||
## v1.1.0 - 2025-01-22
|
## v1.1.0 - 2025-01-22
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
941
Cargo.lock
generated
941
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -13,7 +13,7 @@ categories = ["command-line-utilities"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ratatui = "0.29.0"
|
ratatui = "0.29.0"
|
||||||
crossterm = {version = "0.28.1", features = ["event-stream", "serde"] }
|
crossterm = { version = "0.28.1", features = ["event-stream", "serde"] }
|
||||||
color-eyre = "0.6.2"
|
color-eyre = "0.6.2"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
@ -27,3 +27,4 @@ tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
|||||||
directories = "5.0.1"
|
directories = "5.0.1"
|
||||||
clap = { version = "4.5.23", features = ["derive"] }
|
clap = { version = "4.5.23", features = ["derive"] }
|
||||||
time = { version = "0.3.37", features = ["formatting", "local-offset"] }
|
time = { version = "0.3.37", features = ["formatting", "local-offset"] }
|
||||||
|
notify-rust = "4.11.4"
|
||||||
|
|||||||
17
README.md
17
README.md
@ -68,14 +68,15 @@ timr-tui --help
|
|||||||
Usage: timr-tui [OPTIONS]
|
Usage: timr-tui [OPTIONS]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-c, --countdown <COUNTDOWN> Countdown time to start from. Formats: 'ss', 'mm:ss', or 'hh:mm:ss' [default: 10:00]
|
-c, --countdown <COUNTDOWN> Countdown time to start from. Formats: 'ss', 'mm:ss', or 'hh:mm:ss'
|
||||||
-w, --work <WORK> Work time to count down from. Formats: 'ss', 'mm:ss', or 'hh:mm:ss' [default: 25:00]
|
-w, --work <WORK> Work time to count down from. Formats: 'ss', 'mm:ss', or 'hh:mm:ss'
|
||||||
-p, --pause <PAUSE> Pause time to count down from. Formats: 'ss', 'mm:ss', or 'hh:mm:ss' [default: 5:00]
|
-p, --pause <PAUSE> Pause time to count down from. Formats: 'ss', 'mm:ss', or 'hh:mm:ss'
|
||||||
-d, --decis Wether to show deciseconds or not. [default: false]
|
-d, --decis Show deciseconds.
|
||||||
-m, --mode <MODE> Mode to start with. [possible values: countdown, timer, pomodoro] [default: timer]
|
-m, --mode <MODE> Mode to start with. [possible values: countdown, timer, pomodoro]
|
||||||
--menu Whether to open the menu or not.
|
-s, --style <STYLE> Style to display time with. [possible values: full, light, medium, dark, thick, cross, braille]
|
||||||
-s, --style <STYLE> Style to display time with. [possible values: full, light, medium, dark, thick, cross, braille] [default: full]
|
--menu Open the menu.
|
||||||
-r, --reset Reset stored values to default.
|
-r, --reset Reset stored values to default values.
|
||||||
|
-n, --notification <NOTIFICATION> Toggle desktop notifications on or off. Experimental. [possible values: on, off]
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
-V, --version Print version
|
-V, --version Print version
|
||||||
```
|
```
|
||||||
|
|||||||
44
src/app.rs
44
src/app.rs
@ -1,13 +1,13 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
args::Args,
|
args::Args,
|
||||||
common::{AppEditMode, AppTime, AppTimeFormat, Content, Style},
|
common::{AppEditMode, AppTime, AppTimeFormat, Content, Notification, Style},
|
||||||
constants::TICK_VALUE_MS,
|
constants::TICK_VALUE_MS,
|
||||||
events::{Event, EventHandler, Events},
|
events::{Event, EventHandler, Events},
|
||||||
storage::AppStorage,
|
storage::AppStorage,
|
||||||
terminal::Terminal,
|
terminal::Terminal,
|
||||||
widgets::{
|
widgets::{
|
||||||
clock::{self, ClockState, ClockStateArgs},
|
clock::{self, ClockState, ClockStateArgs},
|
||||||
countdown::{Countdown, CountdownState},
|
countdown::{Countdown, CountdownState, CountdownStateArgs},
|
||||||
footer::{Footer, FooterState},
|
footer::{Footer, FooterState},
|
||||||
header::Header,
|
header::Header,
|
||||||
pomodoro::{Mode as PomodoroMode, PomodoroState, PomodoroStateArgs, PomodoroWidget},
|
pomodoro::{Mode as PomodoroMode, PomodoroState, PomodoroStateArgs, PomodoroWidget},
|
||||||
@ -23,7 +23,7 @@ use ratatui::{
|
|||||||
};
|
};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use tracing::debug;
|
use tracing::{debug, error};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum Mode {
|
enum Mode {
|
||||||
@ -31,10 +31,10 @@ enum Mode {
|
|||||||
Quit,
|
Quit,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
content: Content,
|
content: Content,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
|
notification: Notification,
|
||||||
app_time: AppTime,
|
app_time: AppTime,
|
||||||
countdown: CountdownState,
|
countdown: CountdownState,
|
||||||
timer: TimerState,
|
timer: TimerState,
|
||||||
@ -47,6 +47,7 @@ pub struct App {
|
|||||||
pub struct AppArgs {
|
pub struct AppArgs {
|
||||||
pub style: Style,
|
pub style: Style,
|
||||||
pub with_decis: bool,
|
pub with_decis: bool,
|
||||||
|
pub notification: Notification,
|
||||||
pub show_menu: bool,
|
pub show_menu: bool,
|
||||||
pub app_time_format: AppTimeFormat,
|
pub app_time_format: AppTimeFormat,
|
||||||
pub content: Content,
|
pub content: Content,
|
||||||
@ -68,6 +69,7 @@ impl From<(Args, AppStorage)> for AppArgs {
|
|||||||
AppArgs {
|
AppArgs {
|
||||||
with_decis: args.decis || stg.with_decis,
|
with_decis: args.decis || stg.with_decis,
|
||||||
show_menu: args.menu || stg.show_menu,
|
show_menu: args.menu || stg.show_menu,
|
||||||
|
notification: args.notification.unwrap_or(stg.notification),
|
||||||
app_time_format: stg.app_time_format,
|
app_time_format: stg.app_time_format,
|
||||||
content: args.mode.unwrap_or(stg.content),
|
content: args.mode.unwrap_or(stg.content),
|
||||||
style: args.style.unwrap_or(stg.style),
|
style: args.style.unwrap_or(stg.style),
|
||||||
@ -111,30 +113,44 @@ impl App {
|
|||||||
content,
|
content,
|
||||||
with_decis,
|
with_decis,
|
||||||
pomodoro_mode,
|
pomodoro_mode,
|
||||||
|
notification,
|
||||||
} = args;
|
} = args;
|
||||||
let app_time = get_app_time();
|
let app_time = get_app_time();
|
||||||
Self {
|
Self {
|
||||||
mode: Mode::Running,
|
mode: Mode::Running,
|
||||||
|
notification,
|
||||||
content,
|
content,
|
||||||
app_time,
|
app_time,
|
||||||
style,
|
style,
|
||||||
with_decis,
|
with_decis,
|
||||||
countdown: CountdownState::new(
|
countdown: CountdownState::new(CountdownStateArgs {
|
||||||
ClockState::<clock::Countdown>::new(ClockStateArgs {
|
|
||||||
initial_value: initial_value_countdown,
|
initial_value: initial_value_countdown,
|
||||||
current_value: current_value_countdown,
|
current_value: current_value_countdown,
|
||||||
tick_value: Duration::from_millis(TICK_VALUE_MS),
|
elapsed_value: elapsed_value_countdown,
|
||||||
with_decis,
|
|
||||||
}),
|
|
||||||
elapsed_value_countdown,
|
|
||||||
app_time,
|
app_time,
|
||||||
),
|
with_decis,
|
||||||
timer: TimerState::new(ClockState::<clock::Timer>::new(ClockStateArgs {
|
with_notification: notification == Notification::On,
|
||||||
|
}),
|
||||||
|
timer: TimerState::new(
|
||||||
|
ClockState::<clock::Timer>::new(ClockStateArgs {
|
||||||
initial_value: Duration::ZERO,
|
initial_value: Duration::ZERO,
|
||||||
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,
|
||||||
})),
|
})
|
||||||
|
.with_on_done_by_condition(
|
||||||
|
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,
|
||||||
initial_value_work,
|
initial_value_work,
|
||||||
@ -142,6 +158,7 @@ impl App {
|
|||||||
initial_value_pause,
|
initial_value_pause,
|
||||||
current_value_pause,
|
current_value_pause,
|
||||||
with_decis,
|
with_decis,
|
||||||
|
with_notification: notification == Notification::On,
|
||||||
}),
|
}),
|
||||||
footer: FooterState::new(show_menu, app_time_format),
|
footer: FooterState::new(show_menu, app_time_format),
|
||||||
}
|
}
|
||||||
@ -261,6 +278,7 @@ impl App {
|
|||||||
AppStorage {
|
AppStorage {
|
||||||
content: self.content,
|
content: self.content,
|
||||||
show_menu: self.footer.get_show_menu(),
|
show_menu: self.footer.get_show_menu(),
|
||||||
|
notification: self.notification,
|
||||||
app_time_format: *self.footer.app_time_format(),
|
app_time_format: *self.footer.app_time_format(),
|
||||||
style: self.style,
|
style: self.style,
|
||||||
with_decis: self.with_decis,
|
with_decis: self.with_decis,
|
||||||
|
|||||||
16
src/args.rs
16
src/args.rs
@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
common::{Content, Style},
|
common::{Content, Notification, Style},
|
||||||
duration,
|
duration,
|
||||||
};
|
};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
@ -23,7 +23,7 @@ pub struct Args {
|
|||||||
)]
|
)]
|
||||||
pub pause: Option<Duration>,
|
pub pause: Option<Duration>,
|
||||||
|
|
||||||
#[arg(long, short = 'd', help = "Whether to show deciseconds or not.")]
|
#[arg(long, short = 'd', help = "Show deciseconds.")]
|
||||||
pub decis: bool,
|
pub decis: bool,
|
||||||
|
|
||||||
#[arg(long, short = 'm', value_enum, help = "Mode to start with.")]
|
#[arg(long, short = 'm', value_enum, help = "Mode to start with.")]
|
||||||
@ -32,9 +32,17 @@ pub struct Args {
|
|||||||
#[arg(long, short = 's', value_enum, help = "Style to display time with.")]
|
#[arg(long, short = 's', value_enum, help = "Style to display time with.")]
|
||||||
pub style: Option<Style>,
|
pub style: Option<Style>,
|
||||||
|
|
||||||
#[arg(long, value_enum, help = "Whether to open the menu or not.")]
|
#[arg(long, value_enum, help = "Open the menu.")]
|
||||||
pub menu: bool,
|
pub menu: bool,
|
||||||
|
|
||||||
#[arg(long, short = 'r', help = "Reset stored values to default.")]
|
#[arg(long, short = 'r', help = "Reset stored values to default values.")]
|
||||||
pub reset: bool,
|
pub reset: bool,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
short,
|
||||||
|
value_enum,
|
||||||
|
help = "Toggle desktop notifications on or off. Experimental."
|
||||||
|
)]
|
||||||
|
pub notification: Option<Notification>,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -135,6 +135,15 @@ pub enum AppEditMode {
|
|||||||
Time,
|
Time,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||||
|
pub enum Notification {
|
||||||
|
#[value(name = "on")]
|
||||||
|
On,
|
||||||
|
#[default]
|
||||||
|
#[value(name = "off")]
|
||||||
|
Off,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,10 @@ use color_eyre::eyre::{eyre, Result};
|
|||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
// silence `field `log_dir` is never read` the easy way
|
||||||
|
#[cfg_attr(not(debug_assertions), allow(dead_code))]
|
||||||
pub log_dir: PathBuf,
|
pub log_dir: PathBuf,
|
||||||
pub data_dir: PathBuf,
|
pub data_dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
common::{AppTimeFormat, Content, Style},
|
common::{AppTimeFormat, Content, Notification, Style},
|
||||||
widgets::pomodoro::Mode as PomodoroMode,
|
widgets::pomodoro::Mode as PomodoroMode,
|
||||||
};
|
};
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
@ -12,6 +12,7 @@ use std::time::Duration;
|
|||||||
pub struct AppStorage {
|
pub struct AppStorage {
|
||||||
pub content: Content,
|
pub content: Content,
|
||||||
pub show_menu: bool,
|
pub show_menu: bool,
|
||||||
|
pub notification: Notification,
|
||||||
pub app_time_format: AppTimeFormat,
|
pub app_time_format: AppTimeFormat,
|
||||||
pub style: Style,
|
pub style: Style,
|
||||||
pub with_decis: bool,
|
pub with_decis: bool,
|
||||||
@ -38,6 +39,7 @@ impl Default for AppStorage {
|
|||||||
AppStorage {
|
AppStorage {
|
||||||
content: Content::default(),
|
content: Content::default(),
|
||||||
show_menu: true,
|
show_menu: true,
|
||||||
|
notification: Notification::Off,
|
||||||
app_time_format: AppTimeFormat::default(),
|
app_time_format: AppTimeFormat::default(),
|
||||||
style: Style::default(),
|
style: Style::default(),
|
||||||
with_decis: false,
|
with_decis: false,
|
||||||
|
|||||||
@ -65,7 +65,6 @@ pub enum Format {
|
|||||||
HhMmSs,
|
HhMmSs,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ClockState<T> {
|
pub struct ClockState<T> {
|
||||||
initial_value: DurationEx,
|
initial_value: DurationEx,
|
||||||
current_value: DurationEx,
|
current_value: DurationEx,
|
||||||
@ -73,6 +72,7 @@ pub struct ClockState<T> {
|
|||||||
mode: Mode,
|
mode: Mode,
|
||||||
format: Format,
|
format: Format,
|
||||||
pub with_decis: bool,
|
pub with_decis: bool,
|
||||||
|
on_done: Option<Box<dyn Fn() + 'static>>,
|
||||||
phantom: PhantomData<T>,
|
phantom: PhantomData<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,6 +310,28 @@ 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) {
|
||||||
|
if !self.is_done() {
|
||||||
|
self.mode = Mode::Done;
|
||||||
|
if let Some(handler) = &mut self.on_done {
|
||||||
|
handler();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn update_format(&mut self) {
|
fn update_format(&mut self) {
|
||||||
self.format = self.get_format();
|
self.format = self.get_format();
|
||||||
}
|
}
|
||||||
@ -355,6 +377,7 @@ impl ClockState<Countdown> {
|
|||||||
},
|
},
|
||||||
format: Format::S,
|
format: Format::S,
|
||||||
with_decis,
|
with_decis,
|
||||||
|
on_done: None,
|
||||||
phantom: PhantomData,
|
phantom: PhantomData,
|
||||||
};
|
};
|
||||||
// update format once
|
// update format once
|
||||||
@ -365,14 +388,14 @@ impl ClockState<Countdown> {
|
|||||||
pub fn tick(&mut self) {
|
pub fn tick(&mut self) {
|
||||||
if self.mode == Mode::Tick {
|
if self.mode == Mode::Tick {
|
||||||
self.current_value = self.current_value.saturating_sub(self.tick_value);
|
self.current_value = self.current_value.saturating_sub(self.tick_value);
|
||||||
self.set_done();
|
self.check_done();
|
||||||
self.update_format();
|
self.update_format();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_done(&mut self) {
|
fn check_done(&mut self) {
|
||||||
if self.current_value.eq(&Duration::ZERO.into()) {
|
if self.current_value.eq(&Duration::ZERO.into()) {
|
||||||
self.mode = Mode::Done;
|
self.done();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -422,8 +445,9 @@ impl ClockState<Timer> {
|
|||||||
Mode::Pause
|
Mode::Pause
|
||||||
},
|
},
|
||||||
format: Format::S,
|
format: Format::S,
|
||||||
phantom: PhantomData,
|
|
||||||
with_decis,
|
with_decis,
|
||||||
|
on_done: None,
|
||||||
|
phantom: PhantomData,
|
||||||
};
|
};
|
||||||
// update format once
|
// update format once
|
||||||
instance.update_format();
|
instance.update_format();
|
||||||
@ -433,14 +457,14 @@ impl ClockState<Timer> {
|
|||||||
pub fn tick(&mut self) {
|
pub fn tick(&mut self) {
|
||||||
if self.mode == Mode::Tick {
|
if self.mode == Mode::Tick {
|
||||||
self.current_value = self.current_value.saturating_add(self.tick_value);
|
self.current_value = self.current_value.saturating_add(self.tick_value);
|
||||||
self.set_done();
|
self.check_done();
|
||||||
self.update_format();
|
self.update_format();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_done(&mut self) {
|
fn check_done(&mut self) {
|
||||||
if self.current_value.ge(&MAX_DURATION.into()) {
|
if self.current_value.ge(&MAX_DURATION.into()) {
|
||||||
self.mode = Mode::Done;
|
self.done();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,9 @@ use crate::{
|
|||||||
edit_time::EditTimeState,
|
edit_time::EditTimeState,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
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,
|
||||||
@ -17,6 +19,7 @@ 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};
|
||||||
@ -24,8 +27,16 @@ use time::OffsetDateTime;
|
|||||||
|
|
||||||
use super::edit_time::{EditTimeStateArgs, EditTimeWidget};
|
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
|
/// State for Countdown Widget
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct CountdownState {
|
pub struct CountdownState {
|
||||||
/// clock to count down
|
/// clock to count down
|
||||||
clock: ClockState<clock::Countdown>,
|
clock: ClockState<clock::Countdown>,
|
||||||
@ -37,13 +48,32 @@ pub struct CountdownState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CountdownState {
|
impl CountdownState {
|
||||||
pub fn new(
|
pub fn new(args: CountdownStateArgs) -> Self {
|
||||||
clock: ClockState<clock::Countdown>,
|
let CountdownStateArgs {
|
||||||
elapsed_value: Duration,
|
initial_value,
|
||||||
app_time: AppTime,
|
current_value,
|
||||||
) -> Self {
|
elapsed_value,
|
||||||
|
with_notification,
|
||||||
|
with_decis,
|
||||||
|
app_time,
|
||||||
|
} = args;
|
||||||
|
|
||||||
Self {
|
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 {
|
elapsed_clock: ClockState::<clock::Timer>::new(ClockStateArgs {
|
||||||
initial_value: Duration::ZERO,
|
initial_value: Duration::ZERO,
|
||||||
current_value: elapsed_value,
|
current_value: elapsed_value,
|
||||||
|
|||||||
@ -155,7 +155,7 @@ impl StatefulWidget for Footer {
|
|||||||
if self.selected_content == Content::Countdown {
|
if self.selected_content == Content::Countdown {
|
||||||
spans.extend_from_slice(&[
|
spans.extend_from_slice(&[
|
||||||
Span::from(SPACE),
|
Span::from(SPACE),
|
||||||
Span::from("[ctrl+e]dit by local time"),
|
Span::from("[^e]dit by local time"),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
if self.selected_content == Content::Pomodoro {
|
if self.selected_content == Content::Pomodoro {
|
||||||
@ -169,7 +169,7 @@ impl StatefulWidget for Footer {
|
|||||||
others => vec![
|
others => vec![
|
||||||
Span::from(match others {
|
Span::from(match others {
|
||||||
AppEditMode::Clock => "[e]dit done",
|
AppEditMode::Clock => "[e]dit done",
|
||||||
AppEditMode::Time => "[ctrl+e]dit done",
|
AppEditMode::Time => "[^e]dit done",
|
||||||
_ => "",
|
_ => "",
|
||||||
}),
|
}),
|
||||||
Span::from(SPACE),
|
Span::from(SPACE),
|
||||||
|
|||||||
@ -3,8 +3,9 @@ use crate::{
|
|||||||
constants::TICK_VALUE_MS,
|
constants::TICK_VALUE_MS,
|
||||||
events::{Event, EventHandler},
|
events::{Event, EventHandler},
|
||||||
utils::center,
|
utils::center,
|
||||||
widgets::clock::{ClockState, 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,
|
||||||
@ -12,13 +13,10 @@ use ratatui::{
|
|||||||
text::Line,
|
text::Line,
|
||||||
widgets::{StatefulWidget, Widget},
|
widgets::{StatefulWidget, Widget},
|
||||||
};
|
};
|
||||||
use std::{cmp::max, time::Duration};
|
|
||||||
|
|
||||||
use strum::Display;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{cmp::max, time::Duration};
|
||||||
use super::clock::ClockStateArgs;
|
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 {
|
||||||
@ -26,7 +24,6 @@ pub enum Mode {
|
|||||||
Pause,
|
Pause,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ClockMap {
|
pub struct ClockMap {
|
||||||
work: ClockState<Countdown>,
|
work: ClockState<Countdown>,
|
||||||
pause: ClockState<Countdown>,
|
pause: ClockState<Countdown>,
|
||||||
@ -47,7 +44,6 @@ impl ClockMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct PomodoroState {
|
pub struct PomodoroState {
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
clock_map: ClockMap,
|
clock_map: ClockMap,
|
||||||
@ -60,6 +56,7 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PomodoroState {
|
impl PomodoroState {
|
||||||
@ -71,6 +68,7 @@ impl PomodoroState {
|
|||||||
initial_value_pause,
|
initial_value_pause,
|
||||||
current_value_pause,
|
current_value_pause,
|
||||||
with_decis,
|
with_decis,
|
||||||
|
with_notification,
|
||||||
} = args;
|
} = args;
|
||||||
Self {
|
Self {
|
||||||
mode,
|
mode,
|
||||||
@ -80,12 +78,30 @@ impl PomodoroState {
|
|||||||
current_value: current_value_work,
|
current_value: current_value_work,
|
||||||
tick_value: Duration::from_millis(TICK_VALUE_MS),
|
tick_value: Duration::from_millis(TICK_VALUE_MS),
|
||||||
with_decis,
|
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 {
|
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,
|
||||||
tick_value: Duration::from_millis(TICK_VALUE_MS),
|
tick_value: Duration::from_millis(TICK_VALUE_MS),
|
||||||
with_decis,
|
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;
|
use std::cmp::max;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct TimerState {
|
pub struct TimerState {
|
||||||
clock: ClockState<clock::Timer>,
|
clock: ClockState<clock::Timer>,
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user