parent
6437795c9f
commit
f79813ac6b
28
src/app.rs
28
src/app.rs
@ -9,6 +9,7 @@ use crate::{
|
||||
widgets::{
|
||||
clock::{self, ClockState, ClockStateArgs},
|
||||
countdown::{Countdown, CountdownState, CountdownStateArgs},
|
||||
event::{EventState, EventStateArgs, EventWidget},
|
||||
footer::{Footer, FooterState},
|
||||
header::Header,
|
||||
local_time::{LocalTimeState, LocalTimeStateArgs, LocalTimeWidget},
|
||||
@ -29,6 +30,7 @@ use ratatui::{
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use time::macros::format_description;
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@ -49,6 +51,7 @@ pub struct App {
|
||||
countdown: CountdownState,
|
||||
timer: TimerState,
|
||||
pomodoro: PomodoroState,
|
||||
event: EventState,
|
||||
local_time: LocalTimeState,
|
||||
style: Style,
|
||||
with_decis: bool,
|
||||
@ -223,6 +226,17 @@ impl App {
|
||||
app_time,
|
||||
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(
|
||||
show_menu,
|
||||
if footer_toggle_app_time == Toggle::On {
|
||||
@ -247,6 +261,9 @@ impl App {
|
||||
KeyCode::Char('c') => app.content = Content::Countdown,
|
||||
KeyCode::Char('t') => app.content = Content::Timer,
|
||||
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,
|
||||
// toogle app time format
|
||||
KeyCode::Char(':') => {
|
||||
@ -291,6 +308,7 @@ impl App {
|
||||
app.timer.set_with_decis(app.with_decis);
|
||||
app.countdown.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::Down => app.footer.set_show_menu(false),
|
||||
@ -303,6 +321,7 @@ impl App {
|
||||
app.app_time = AppTime::new();
|
||||
app.countdown.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
|
||||
@ -310,6 +329,7 @@ impl App {
|
||||
Content::Countdown => app.countdown.update(event.clone()),
|
||||
Content::Timer => app.timer.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()),
|
||||
} {
|
||||
match unhandled {
|
||||
@ -401,6 +421,7 @@ impl App {
|
||||
AppEditMode::None
|
||||
}
|
||||
}
|
||||
Content::Event => AppEditMode::None,
|
||||
Content::LocalTime => AppEditMode::None,
|
||||
}
|
||||
}
|
||||
@ -410,6 +431,7 @@ impl App {
|
||||
Content::Countdown => self.countdown.is_running(),
|
||||
Content::Timer => self.timer.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`
|
||||
Content::LocalTime => false,
|
||||
}
|
||||
@ -420,6 +442,7 @@ impl App {
|
||||
Content::Countdown => Some(self.countdown.get_clock().get_percentage_done()),
|
||||
Content::Timer => None,
|
||||
Content::Pomodoro => Some(self.pomodoro.get_clock().get_percentage_done()),
|
||||
Content::Event => Some(self.event.get_percentage_done()),
|
||||
Content::LocalTime => None,
|
||||
}
|
||||
}
|
||||
@ -483,6 +506,11 @@ impl AppWidget {
|
||||
blink: state.blink == Toggle::On,
|
||||
}
|
||||
.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 => {
|
||||
LocalTimeWidget { style: state.style }.render(area, buf, &mut state.local_time);
|
||||
}
|
||||
|
||||
@ -15,6 +15,8 @@ pub enum Content {
|
||||
Timer,
|
||||
#[value(name = "pomodoro", alias = "p")]
|
||||
Pomodoro,
|
||||
#[value(name = "event", alias = "e")]
|
||||
Event,
|
||||
#[value(name = "localtime", alias = "l")]
|
||||
LocalTime,
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ use color_eyre::{
|
||||
use std::cmp::min;
|
||||
use std::fmt;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::common::AppTime;
|
||||
|
||||
@ -46,6 +47,29 @@ pub enum DirectedDuration {
|
||||
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)]
|
||||
pub struct DurationEx {
|
||||
inner: Duration,
|
||||
@ -472,6 +496,31 @@ mod tests {
|
||||
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]
|
||||
fn test_parse_duration() {
|
||||
// ss
|
||||
|
||||
@ -6,6 +6,7 @@ pub mod clock_elements_test;
|
||||
pub mod clock_test;
|
||||
pub mod countdown;
|
||||
pub mod edit_time;
|
||||
pub mod event;
|
||||
pub mod footer;
|
||||
pub mod header;
|
||||
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