parent
6437795c9f
commit
f79813ac6b
28
src/app.rs
28
src/app.rs
@ -9,6 +9,7 @@ use crate::{
|
|||||||
widgets::{
|
widgets::{
|
||||||
clock::{self, ClockState, ClockStateArgs},
|
clock::{self, ClockState, ClockStateArgs},
|
||||||
countdown::{Countdown, CountdownState, CountdownStateArgs},
|
countdown::{Countdown, CountdownState, CountdownStateArgs},
|
||||||
|
event::{EventState, EventStateArgs, EventWidget},
|
||||||
footer::{Footer, FooterState},
|
footer::{Footer, FooterState},
|
||||||
header::Header,
|
header::Header,
|
||||||
local_time::{LocalTimeState, LocalTimeStateArgs, LocalTimeWidget},
|
local_time::{LocalTimeState, LocalTimeStateArgs, LocalTimeWidget},
|
||||||
@ -29,6 +30,7 @@ use ratatui::{
|
|||||||
};
|
};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use time::macros::format_description;
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@ -49,6 +51,7 @@ pub struct App {
|
|||||||
countdown: CountdownState,
|
countdown: CountdownState,
|
||||||
timer: TimerState,
|
timer: TimerState,
|
||||||
pomodoro: PomodoroState,
|
pomodoro: PomodoroState,
|
||||||
|
event: EventState,
|
||||||
local_time: LocalTimeState,
|
local_time: LocalTimeState,
|
||||||
style: Style,
|
style: Style,
|
||||||
with_decis: bool,
|
with_decis: bool,
|
||||||
@ -223,6 +226,17 @@ impl App {
|
|||||||
app_time,
|
app_time,
|
||||||
app_time_format,
|
app_time_format,
|
||||||
}),
|
}),
|
||||||
|
event: EventState::new(EventStateArgs {
|
||||||
|
app_time,
|
||||||
|
event_time: time::PrimitiveDateTime::parse(
|
||||||
|
"2030-10-03 15:00:00",
|
||||||
|
format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
event_title: "My event".to_owned(),
|
||||||
|
with_decis,
|
||||||
|
app_tx: app_tx.clone(),
|
||||||
|
}),
|
||||||
footer: FooterState::new(
|
footer: FooterState::new(
|
||||||
show_menu,
|
show_menu,
|
||||||
if footer_toggle_app_time == Toggle::On {
|
if footer_toggle_app_time == Toggle::On {
|
||||||
@ -247,6 +261,9 @@ impl App {
|
|||||||
KeyCode::Char('c') => app.content = Content::Countdown,
|
KeyCode::Char('c') => app.content = Content::Countdown,
|
||||||
KeyCode::Char('t') => app.content = Content::Timer,
|
KeyCode::Char('t') => app.content = Content::Timer,
|
||||||
KeyCode::Char('p') => app.content = Content::Pomodoro,
|
KeyCode::Char('p') => app.content = Content::Pomodoro,
|
||||||
|
// TODO(#102) Before we can use `e` here
|
||||||
|
// we do need to change keybindings for editing.
|
||||||
|
KeyCode::Char('z') => app.content = Content::Event,
|
||||||
KeyCode::Char('l') => app.content = Content::LocalTime,
|
KeyCode::Char('l') => app.content = Content::LocalTime,
|
||||||
// toogle app time format
|
// toogle app time format
|
||||||
KeyCode::Char(':') => {
|
KeyCode::Char(':') => {
|
||||||
@ -291,6 +308,7 @@ impl App {
|
|||||||
app.timer.set_with_decis(app.with_decis);
|
app.timer.set_with_decis(app.with_decis);
|
||||||
app.countdown.set_with_decis(app.with_decis);
|
app.countdown.set_with_decis(app.with_decis);
|
||||||
app.pomodoro.set_with_decis(app.with_decis);
|
app.pomodoro.set_with_decis(app.with_decis);
|
||||||
|
app.event.set_with_decis(app.with_decis);
|
||||||
}
|
}
|
||||||
KeyCode::Up => app.footer.set_show_menu(true),
|
KeyCode::Up => app.footer.set_show_menu(true),
|
||||||
KeyCode::Down => app.footer.set_show_menu(false),
|
KeyCode::Down => app.footer.set_show_menu(false),
|
||||||
@ -303,6 +321,7 @@ impl App {
|
|||||||
app.app_time = AppTime::new();
|
app.app_time = AppTime::new();
|
||||||
app.countdown.set_app_time(app.app_time);
|
app.countdown.set_app_time(app.app_time);
|
||||||
app.local_time.set_app_time(app.app_time);
|
app.local_time.set_app_time(app.app_time);
|
||||||
|
app.event.set_app_time(app.app_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pipe events into subviews and handle only 'unhandled' events afterwards
|
// Pipe events into subviews and handle only 'unhandled' events afterwards
|
||||||
@ -310,6 +329,7 @@ impl App {
|
|||||||
Content::Countdown => app.countdown.update(event.clone()),
|
Content::Countdown => app.countdown.update(event.clone()),
|
||||||
Content::Timer => app.timer.update(event.clone()),
|
Content::Timer => app.timer.update(event.clone()),
|
||||||
Content::Pomodoro => app.pomodoro.update(event.clone()),
|
Content::Pomodoro => app.pomodoro.update(event.clone()),
|
||||||
|
Content::Event => app.event.update(event.clone()),
|
||||||
Content::LocalTime => app.local_time.update(event.clone()),
|
Content::LocalTime => app.local_time.update(event.clone()),
|
||||||
} {
|
} {
|
||||||
match unhandled {
|
match unhandled {
|
||||||
@ -401,6 +421,7 @@ impl App {
|
|||||||
AppEditMode::None
|
AppEditMode::None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Content::Event => AppEditMode::None,
|
||||||
Content::LocalTime => AppEditMode::None,
|
Content::LocalTime => AppEditMode::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -410,6 +431,7 @@ impl App {
|
|||||||
Content::Countdown => self.countdown.is_running(),
|
Content::Countdown => self.countdown.is_running(),
|
||||||
Content::Timer => self.timer.get_clock().is_running(),
|
Content::Timer => self.timer.get_clock().is_running(),
|
||||||
Content::Pomodoro => self.pomodoro.get_clock().is_running(),
|
Content::Pomodoro => self.pomodoro.get_clock().is_running(),
|
||||||
|
Content::Event => self.event.get_clock().is_running(),
|
||||||
// `LocalTime` does not use a `Clock`
|
// `LocalTime` does not use a `Clock`
|
||||||
Content::LocalTime => false,
|
Content::LocalTime => false,
|
||||||
}
|
}
|
||||||
@ -420,6 +442,7 @@ impl App {
|
|||||||
Content::Countdown => Some(self.countdown.get_clock().get_percentage_done()),
|
Content::Countdown => Some(self.countdown.get_clock().get_percentage_done()),
|
||||||
Content::Timer => None,
|
Content::Timer => None,
|
||||||
Content::Pomodoro => Some(self.pomodoro.get_clock().get_percentage_done()),
|
Content::Pomodoro => Some(self.pomodoro.get_clock().get_percentage_done()),
|
||||||
|
Content::Event => Some(self.event.get_percentage_done()),
|
||||||
Content::LocalTime => None,
|
Content::LocalTime => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -483,6 +506,11 @@ impl AppWidget {
|
|||||||
blink: state.blink == Toggle::On,
|
blink: state.blink == Toggle::On,
|
||||||
}
|
}
|
||||||
.render(area, buf, &mut state.pomodoro),
|
.render(area, buf, &mut state.pomodoro),
|
||||||
|
Content::Event => EventWidget {
|
||||||
|
style: state.style,
|
||||||
|
blink: state.blink == Toggle::On,
|
||||||
|
}
|
||||||
|
.render(area, buf, &mut state.event),
|
||||||
Content::LocalTime => {
|
Content::LocalTime => {
|
||||||
LocalTimeWidget { style: state.style }.render(area, buf, &mut state.local_time);
|
LocalTimeWidget { style: state.style }.render(area, buf, &mut state.local_time);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,8 @@ pub enum Content {
|
|||||||
Timer,
|
Timer,
|
||||||
#[value(name = "pomodoro", alias = "p")]
|
#[value(name = "pomodoro", alias = "p")]
|
||||||
Pomodoro,
|
Pomodoro,
|
||||||
|
#[value(name = "event", alias = "e")]
|
||||||
|
Event,
|
||||||
#[value(name = "localtime", alias = "l")]
|
#[value(name = "localtime", alias = "l")]
|
||||||
LocalTime,
|
LocalTime,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ use color_eyre::{
|
|||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
use crate::common::AppTime;
|
use crate::common::AppTime;
|
||||||
|
|
||||||
@ -46,6 +47,29 @@ pub enum DirectedDuration {
|
|||||||
Since(Duration),
|
Since(Duration),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<DirectedDuration> for Duration {
|
||||||
|
fn from(directed: DirectedDuration) -> Self {
|
||||||
|
match directed {
|
||||||
|
DirectedDuration::Until(d) => d,
|
||||||
|
DirectedDuration::Since(d) => d,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DirectedDuration {
|
||||||
|
pub fn from_offset_date_times(value_a: OffsetDateTime, value_b: OffsetDateTime) -> Self {
|
||||||
|
let diff = value_a - value_b;
|
||||||
|
|
||||||
|
if diff.is_negative() {
|
||||||
|
Self::Since(Duration::from_millis(
|
||||||
|
diff.whole_milliseconds().unsigned_abs() as u64,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Self::Until(Duration::from_millis(diff.whole_milliseconds() as u64))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialOrd)]
|
#[derive(Debug, Clone, Copy, PartialOrd)]
|
||||||
pub struct DurationEx {
|
pub struct DurationEx {
|
||||||
inner: Duration,
|
inner: Duration,
|
||||||
@ -472,6 +496,31 @@ mod tests {
|
|||||||
assert_eq!(result, 1);
|
assert_eq!(result, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_offset_date_times() {
|
||||||
|
use time::macros::datetime;
|
||||||
|
|
||||||
|
// Future time (Until)
|
||||||
|
let now = datetime!(2024-01-01 12:00:00).assume_utc();
|
||||||
|
let future = datetime!(2024-01-01 13:00:00).assume_utc();
|
||||||
|
assert!(matches!(
|
||||||
|
DirectedDuration::from_offset_date_times(future, now),
|
||||||
|
DirectedDuration::Until(_)
|
||||||
|
));
|
||||||
|
|
||||||
|
// Past time (Since)
|
||||||
|
assert!(matches!(
|
||||||
|
DirectedDuration::from_offset_date_times(now, future),
|
||||||
|
DirectedDuration::Since(_)
|
||||||
|
));
|
||||||
|
|
||||||
|
// Same time (Until with 0 duration)
|
||||||
|
assert!(matches!(
|
||||||
|
DirectedDuration::from_offset_date_times(now, now),
|
||||||
|
DirectedDuration::Until(_)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_duration() {
|
fn test_parse_duration() {
|
||||||
// ss
|
// ss
|
||||||
|
|||||||
@ -6,6 +6,7 @@ pub mod clock_elements_test;
|
|||||||
pub mod clock_test;
|
pub mod clock_test;
|
||||||
pub mod countdown;
|
pub mod countdown;
|
||||||
pub mod edit_time;
|
pub mod edit_time;
|
||||||
|
pub mod event;
|
||||||
pub mod footer;
|
pub mod footer;
|
||||||
pub mod header;
|
pub mod header;
|
||||||
pub mod local_time;
|
pub mod local_time;
|
||||||
|
|||||||
156
src/widgets/event.rs
Normal file
156
src/widgets/event.rs
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::{Constraint, Layout, Rect},
|
||||||
|
text::Line,
|
||||||
|
widgets::{StatefulWidget, Widget},
|
||||||
|
};
|
||||||
|
use time::{OffsetDateTime, macros::format_description};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
common::{AppTime, Style},
|
||||||
|
constants::TICK_VALUE_MS,
|
||||||
|
duration::DirectedDuration,
|
||||||
|
events::{AppEventTx, TuiEvent, TuiEventHandler},
|
||||||
|
utils::center,
|
||||||
|
widgets::clock::{self, ClockState, ClockStateArgs, ClockWidget},
|
||||||
|
};
|
||||||
|
use std::{cmp::max, time::Duration};
|
||||||
|
|
||||||
|
/// State for `EventWidget`
|
||||||
|
pub struct EventState {
|
||||||
|
title: String,
|
||||||
|
event_time: OffsetDateTime,
|
||||||
|
clock: ClockState<clock::Countdown>,
|
||||||
|
directed_duration: DirectedDuration,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EventStateArgs {
|
||||||
|
pub app_time: AppTime,
|
||||||
|
pub event_time: time::PrimitiveDateTime,
|
||||||
|
pub event_title: String,
|
||||||
|
pub with_decis: bool,
|
||||||
|
pub app_tx: AppEventTx,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventState {
|
||||||
|
pub fn new(args: EventStateArgs) -> Self {
|
||||||
|
let EventStateArgs {
|
||||||
|
app_time,
|
||||||
|
event_time,
|
||||||
|
event_title,
|
||||||
|
with_decis,
|
||||||
|
app_tx,
|
||||||
|
} = args;
|
||||||
|
|
||||||
|
let app_datetime = OffsetDateTime::from(app_time);
|
||||||
|
// assume event has as same `offset` as `app_time`
|
||||||
|
let event_offset = event_time.assume_offset(app_datetime.offset());
|
||||||
|
let directed_duration =
|
||||||
|
DirectedDuration::from_offset_date_times(event_offset, app_datetime);
|
||||||
|
let current_value = directed_duration.into();
|
||||||
|
|
||||||
|
let clock = ClockState::<clock::Countdown>::new(ClockStateArgs {
|
||||||
|
initial_value: current_value,
|
||||||
|
current_value,
|
||||||
|
tick_value: Duration::from_millis(TICK_VALUE_MS),
|
||||||
|
with_decis,
|
||||||
|
app_tx: Some(app_tx.clone()),
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
title: event_title,
|
||||||
|
event_time: event_offset,
|
||||||
|
directed_duration,
|
||||||
|
clock,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_clock(&self) -> &ClockState<clock::Countdown> {
|
||||||
|
&self.clock
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_app_time(&mut self, app_time: AppTime) {
|
||||||
|
// update `directed_duration`
|
||||||
|
let app_datetime = OffsetDateTime::from(app_time);
|
||||||
|
self.directed_duration =
|
||||||
|
DirectedDuration::from_offset_date_times(self.event_time, app_datetime);
|
||||||
|
// update clock
|
||||||
|
let duration: Duration = self.directed_duration.into();
|
||||||
|
self.clock.set_current_value(duration.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_with_decis(&mut self, with_decis: bool) {
|
||||||
|
self.clock.with_decis = with_decis;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_percentage_done(&self) -> u16 {
|
||||||
|
match self.directed_duration {
|
||||||
|
DirectedDuration::Since(_) => 100,
|
||||||
|
DirectedDuration::Until(_) => self.clock.get_percentage_done(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TuiEventHandler for EventState {
|
||||||
|
fn update(&mut self, event: TuiEvent) -> Option<TuiEvent> {
|
||||||
|
Some(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct EventWidget {
|
||||||
|
pub style: Style,
|
||||||
|
pub blink: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatefulWidget for EventWidget {
|
||||||
|
type State = EventState;
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||||
|
let clock = &mut state.clock;
|
||||||
|
let clock_widget = ClockWidget::new(self.style, self.blink);
|
||||||
|
let label_event = Line::raw(state.title.to_uppercase());
|
||||||
|
let time_str = state
|
||||||
|
.event_time
|
||||||
|
.format(&format_description!(
|
||||||
|
"[year]-[month]-[day] [hour]:[minute]:[second]"
|
||||||
|
))
|
||||||
|
.unwrap_or_else(|e| format!("time format error: {}", e));
|
||||||
|
let time_prefix = match state.directed_duration {
|
||||||
|
DirectedDuration::Since(d) => {
|
||||||
|
// Show `done` for a short of time (1 sec.)
|
||||||
|
if d < Duration::from_secs(1) {
|
||||||
|
"Done"
|
||||||
|
} else {
|
||||||
|
"Since"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DirectedDuration::Until(_) => "Until",
|
||||||
|
};
|
||||||
|
let label_time = Line::raw(format!(
|
||||||
|
"{} {}",
|
||||||
|
time_prefix.to_uppercase(),
|
||||||
|
time_str.to_uppercase()
|
||||||
|
));
|
||||||
|
let max_label_width = max(label_event.width(), label_time.width()) as u16;
|
||||||
|
|
||||||
|
let area = center(
|
||||||
|
area,
|
||||||
|
Constraint::Length(max(
|
||||||
|
clock_widget.get_width(clock.get_format(), clock.with_decis),
|
||||||
|
max_label_width,
|
||||||
|
)),
|
||||||
|
Constraint::Length(clock_widget.get_height() + 3 /* height of label */),
|
||||||
|
);
|
||||||
|
let [_, v1, v2, v3] = Layout::vertical(Constraint::from_lengths([
|
||||||
|
1, // empty (offset) to keep everything centered vertically comparing to "clock" widgets with one label only
|
||||||
|
clock_widget.get_height(),
|
||||||
|
1, // event date
|
||||||
|
1, // event title
|
||||||
|
]))
|
||||||
|
.areas(area);
|
||||||
|
|
||||||
|
clock_widget.render(v1, buf, clock);
|
||||||
|
label_time.centered().render(v2, buf);
|
||||||
|
label_event.centered().render(v3, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user