Edit countdown by local time (#49)
This commit is contained in:
parent
b1efb1eb62
commit
6d2bf5ac09
38
src/app.rs
38
src/app.rs
@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
args::Args,
|
args::Args,
|
||||||
common::{AppTime, AppTimeFormat, Content, Style},
|
common::{AppEditMode, AppTime, AppTimeFormat, Content, Style},
|
||||||
constants::TICK_VALUE_MS,
|
constants::TICK_VALUE_MS,
|
||||||
events::{Event, EventHandler, Events},
|
events::{Event, EventHandler, Events},
|
||||||
storage::AppStorage,
|
storage::AppStorage,
|
||||||
@ -112,10 +112,11 @@ impl App {
|
|||||||
with_decis,
|
with_decis,
|
||||||
pomodoro_mode,
|
pomodoro_mode,
|
||||||
} = args;
|
} = args;
|
||||||
|
let app_time = get_app_time();
|
||||||
Self {
|
Self {
|
||||||
mode: Mode::Running,
|
mode: Mode::Running,
|
||||||
content,
|
content,
|
||||||
app_time: get_app_time(),
|
app_time,
|
||||||
style,
|
style,
|
||||||
with_decis,
|
with_decis,
|
||||||
countdown: CountdownState::new(
|
countdown: CountdownState::new(
|
||||||
@ -126,6 +127,7 @@ impl App {
|
|||||||
with_decis,
|
with_decis,
|
||||||
}),
|
}),
|
||||||
elapsed_value_countdown,
|
elapsed_value_countdown,
|
||||||
|
app_time,
|
||||||
),
|
),
|
||||||
timer: TimerState::new(ClockState::<clock::Timer>::new(ClockStateArgs {
|
timer: TimerState::new(ClockState::<clock::Timer>::new(ClockStateArgs {
|
||||||
initial_value: Duration::ZERO,
|
initial_value: Duration::ZERO,
|
||||||
@ -150,6 +152,7 @@ impl App {
|
|||||||
if let Some(event) = events.next().await {
|
if let Some(event) = events.next().await {
|
||||||
if matches!(event, Event::Tick) {
|
if matches!(event, Event::Tick) {
|
||||||
self.app_time = get_app_time();
|
self.app_time = get_app_time();
|
||||||
|
self.countdown.set_app_time(self.app_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pipe events into subviews and handle only 'unhandled' events afterwards
|
// Pipe events into subviews and handle only 'unhandled' events afterwards
|
||||||
@ -175,11 +178,32 @@ impl App {
|
|||||||
self.mode != Mode::Quit
|
self.mode != Mode::Quit
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_edit_mode(&self) -> bool {
|
fn get_edit_mode(&self) -> AppEditMode {
|
||||||
match self.content {
|
match self.content {
|
||||||
Content::Countdown => self.countdown.get_clock().is_edit_mode(),
|
Content::Countdown => {
|
||||||
Content::Timer => self.timer.get_clock().is_edit_mode(),
|
if self.countdown.is_clock_edit_mode() {
|
||||||
Content::Pomodoro => self.pomodoro.get_clock().is_edit_mode(),
|
AppEditMode::Clock
|
||||||
|
} else if self.countdown.is_time_edit_mode() {
|
||||||
|
AppEditMode::Time
|
||||||
|
} else {
|
||||||
|
AppEditMode::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Content::Timer => {
|
||||||
|
if self.timer.get_clock().is_edit_mode() {
|
||||||
|
AppEditMode::Clock
|
||||||
|
} else {
|
||||||
|
AppEditMode::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Content::Pomodoro => {
|
||||||
|
if self.pomodoro.get_clock().is_edit_mode() {
|
||||||
|
AppEditMode::Clock
|
||||||
|
} else {
|
||||||
|
AppEditMode::None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -298,7 +322,7 @@ impl StatefulWidget for AppWidget {
|
|||||||
Footer {
|
Footer {
|
||||||
running_clock: state.clock_is_running(),
|
running_clock: state.clock_is_running(),
|
||||||
selected_content: state.content,
|
selected_content: state.content,
|
||||||
edit_mode: state.is_edit_mode(),
|
app_edit_mode: state.get_edit_mode(),
|
||||||
app_time: state.app_time,
|
app_time: state.app_time,
|
||||||
}
|
}
|
||||||
.render(v2, buf, &mut state.footer);
|
.render(v2, buf, &mut state.footer);
|
||||||
|
|||||||
@ -128,6 +128,13 @@ impl AppTime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum AppEditMode {
|
||||||
|
None,
|
||||||
|
Clock,
|
||||||
|
Time,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,10 @@ pub const MINS_PER_HOUR: u64 = 60;
|
|||||||
// https://doc.rust-lang.org/src/core/time.rs.html#36
|
// https://doc.rust-lang.org/src/core/time.rs.html#36
|
||||||
const HOURS_PER_DAY: u64 = 24;
|
const HOURS_PER_DAY: u64 = 24;
|
||||||
|
|
||||||
|
// max. 99:59:59
|
||||||
|
pub const MAX_DURATION: Duration =
|
||||||
|
Duration::from_secs(100 * MINS_PER_HOUR * SECS_PER_MINUTE).saturating_sub(ONE_SECOND);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialOrd)]
|
#[derive(Debug, Clone, Copy, PartialOrd)]
|
||||||
pub struct DurationEx {
|
pub struct DurationEx {
|
||||||
inner: Duration,
|
inner: Duration,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ pub mod clock_elements_test;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod clock_test;
|
pub mod clock_test;
|
||||||
pub mod countdown;
|
pub mod countdown;
|
||||||
|
pub mod edit_time;
|
||||||
pub mod footer;
|
pub mod footer;
|
||||||
pub mod header;
|
pub mod header;
|
||||||
pub mod pomodoro;
|
pub mod pomodoro;
|
||||||
|
|||||||
@ -11,20 +11,13 @@ use ratatui::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
common::Style,
|
common::Style,
|
||||||
duration::{
|
duration::{DurationEx, MAX_DURATION, ONE_DECI_SECOND, ONE_HOUR, ONE_MINUTE, ONE_SECOND},
|
||||||
DurationEx, MINS_PER_HOUR, ONE_DECI_SECOND, ONE_HOUR, ONE_MINUTE, ONE_SECOND,
|
|
||||||
SECS_PER_MINUTE,
|
|
||||||
},
|
|
||||||
utils::center_horizontal,
|
utils::center_horizontal,
|
||||||
widgets::clock_elements::{
|
widgets::clock_elements::{
|
||||||
Colon, Digit, Dot, COLON_WIDTH, DIGIT_HEIGHT, DIGIT_WIDTH, DOT_WIDTH,
|
Colon, Digit, Dot, COLON_WIDTH, DIGIT_HEIGHT, DIGIT_SPACE_WIDTH, DIGIT_WIDTH, DOT_WIDTH,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// max. 99:59:59
|
|
||||||
const MAX_DURATION: Duration =
|
|
||||||
Duration::from_secs(100 * MINS_PER_HOUR * SECS_PER_MINUTE).saturating_sub(ONE_SECOND);
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Display, PartialEq, Eq)]
|
#[derive(Debug, Copy, Clone, Display, PartialEq, Eq)]
|
||||||
pub enum Time {
|
pub enum Time {
|
||||||
Decis,
|
Decis,
|
||||||
@ -128,6 +121,11 @@ impl<T> ClockState<T> {
|
|||||||
&self.current_value
|
&self.current_value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_current_value(&mut self, duration: DurationEx) {
|
||||||
|
self.current_value = duration;
|
||||||
|
self.update_format();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn toggle_edit(&mut self) {
|
pub fn toggle_edit(&mut self) {
|
||||||
self.mode = match self.mode.clone() {
|
self.mode = match self.mode.clone() {
|
||||||
Mode::Editable(_, prev) => {
|
Mode::Editable(_, prev) => {
|
||||||
@ -463,8 +461,6 @@ impl ClockState<Timer> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SPACE_WIDTH: u16 = 1;
|
|
||||||
|
|
||||||
pub struct ClockWidget<T>
|
pub struct ClockWidget<T>
|
||||||
where
|
where
|
||||||
T: std::fmt::Debug,
|
T: std::fmt::Debug,
|
||||||
@ -498,61 +494,61 @@ where
|
|||||||
match format {
|
match format {
|
||||||
Format::HhMmSs => add_decis(
|
Format::HhMmSs => add_decis(
|
||||||
vec![
|
vec![
|
||||||
DIGIT_WIDTH, // h
|
DIGIT_WIDTH, // h
|
||||||
SPACE_WIDTH, // (space)
|
DIGIT_SPACE_WIDTH, // (space)
|
||||||
DIGIT_WIDTH, // h
|
DIGIT_WIDTH, // h
|
||||||
COLON_WIDTH, // :
|
COLON_WIDTH, // :
|
||||||
DIGIT_WIDTH, // m
|
DIGIT_WIDTH, // m
|
||||||
SPACE_WIDTH, // (space)
|
DIGIT_SPACE_WIDTH, // (space)
|
||||||
DIGIT_WIDTH, // m
|
DIGIT_WIDTH, // m
|
||||||
COLON_WIDTH, // :
|
COLON_WIDTH, // :
|
||||||
DIGIT_WIDTH, // s
|
DIGIT_WIDTH, // s
|
||||||
SPACE_WIDTH, // (space)
|
DIGIT_SPACE_WIDTH, // (space)
|
||||||
DIGIT_WIDTH, // s
|
DIGIT_WIDTH, // s
|
||||||
],
|
],
|
||||||
with_decis,
|
with_decis,
|
||||||
),
|
),
|
||||||
Format::HMmSs => add_decis(
|
Format::HMmSs => add_decis(
|
||||||
vec![
|
vec![
|
||||||
DIGIT_WIDTH, // h
|
DIGIT_WIDTH, // h
|
||||||
COLON_WIDTH, // :
|
COLON_WIDTH, // :
|
||||||
DIGIT_WIDTH, // m
|
DIGIT_WIDTH, // m
|
||||||
SPACE_WIDTH, // (space)
|
DIGIT_SPACE_WIDTH, // (space)
|
||||||
DIGIT_WIDTH, // m
|
DIGIT_WIDTH, // m
|
||||||
COLON_WIDTH, // :
|
COLON_WIDTH, // :
|
||||||
DIGIT_WIDTH, // s
|
DIGIT_WIDTH, // s
|
||||||
SPACE_WIDTH, // (space)
|
DIGIT_SPACE_WIDTH, // (space)
|
||||||
DIGIT_WIDTH, // s
|
DIGIT_WIDTH, // s
|
||||||
],
|
],
|
||||||
with_decis,
|
with_decis,
|
||||||
),
|
),
|
||||||
Format::MmSs => add_decis(
|
Format::MmSs => add_decis(
|
||||||
vec![
|
vec![
|
||||||
DIGIT_WIDTH, // m
|
DIGIT_WIDTH, // m
|
||||||
SPACE_WIDTH, // (space)
|
DIGIT_SPACE_WIDTH, // (space)
|
||||||
DIGIT_WIDTH, // m
|
DIGIT_WIDTH, // m
|
||||||
COLON_WIDTH, // :
|
COLON_WIDTH, // :
|
||||||
DIGIT_WIDTH, // s
|
DIGIT_WIDTH, // s
|
||||||
SPACE_WIDTH, // (space)
|
DIGIT_SPACE_WIDTH, // (space)
|
||||||
DIGIT_WIDTH, // s
|
DIGIT_WIDTH, // s
|
||||||
],
|
],
|
||||||
with_decis,
|
with_decis,
|
||||||
),
|
),
|
||||||
Format::MSs => add_decis(
|
Format::MSs => add_decis(
|
||||||
vec![
|
vec![
|
||||||
DIGIT_WIDTH, // m
|
DIGIT_WIDTH, // m
|
||||||
COLON_WIDTH, // :
|
COLON_WIDTH, // :
|
||||||
DIGIT_WIDTH, // s
|
DIGIT_WIDTH, // s
|
||||||
SPACE_WIDTH, // (space)
|
DIGIT_SPACE_WIDTH, // (space)
|
||||||
DIGIT_WIDTH, // s
|
DIGIT_WIDTH, // s
|
||||||
],
|
],
|
||||||
with_decis,
|
with_decis,
|
||||||
),
|
),
|
||||||
Format::Ss => add_decis(
|
Format::Ss => add_decis(
|
||||||
vec![
|
vec![
|
||||||
DIGIT_WIDTH, // s
|
DIGIT_WIDTH, // s
|
||||||
SPACE_WIDTH, // (space)
|
DIGIT_SPACE_WIDTH, // (space)
|
||||||
DIGIT_WIDTH, // s
|
DIGIT_WIDTH, // s
|
||||||
],
|
],
|
||||||
with_decis,
|
with_decis,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -9,6 +9,7 @@ pub const DIGIT_WIDTH: u16 = DIGIT_SIZE as u16;
|
|||||||
pub const DIGIT_HEIGHT: u16 = DIGIT_SIZE as u16 + 1 /* border height */;
|
pub const DIGIT_HEIGHT: u16 = DIGIT_SIZE as u16 + 1 /* border height */;
|
||||||
pub const COLON_WIDTH: u16 = 4; // incl. padding left + padding right
|
pub const COLON_WIDTH: u16 = 4; // incl. padding left + padding right
|
||||||
pub const DOT_WIDTH: u16 = 4; // incl. padding left + padding right
|
pub const DOT_WIDTH: u16 = 4; // incl. padding left + padding right
|
||||||
|
pub const DIGIT_SPACE_WIDTH: u16 = 1; // space between digits
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
const DIGIT_0: [u8; DIGIT_SIZE * DIGIT_SIZE] = [
|
const DIGIT_0: [u8; DIGIT_SIZE * DIGIT_SIZE] = [
|
||||||
|
|||||||
@ -1,3 +1,15 @@
|
|||||||
|
use crate::{
|
||||||
|
common::{AppTime, Style},
|
||||||
|
constants::TICK_VALUE_MS,
|
||||||
|
duration::{DurationEx, MAX_DURATION},
|
||||||
|
events::{Event, EventHandler},
|
||||||
|
utils::center,
|
||||||
|
widgets::{
|
||||||
|
clock::{self, ClockState, ClockStateArgs, ClockWidget, Mode as ClockMode},
|
||||||
|
edit_time::EditTimeState,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use crossterm::event::KeyModifiers;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
crossterm::event::KeyCode,
|
crossterm::event::KeyCode,
|
||||||
@ -5,16 +17,12 @@ use ratatui::{
|
|||||||
text::Line,
|
text::Line,
|
||||||
widgets::{StatefulWidget, Widget},
|
widgets::{StatefulWidget, Widget},
|
||||||
};
|
};
|
||||||
use std::{cmp::max, time::Duration};
|
|
||||||
|
|
||||||
use crate::{
|
use std::ops::Sub;
|
||||||
common::Style,
|
use std::{cmp::max, time::Duration};
|
||||||
constants::TICK_VALUE_MS,
|
use time::OffsetDateTime;
|
||||||
duration::DurationEx,
|
|
||||||
events::{Event, EventHandler},
|
use super::edit_time::{EditTimeStateArgs, EditTimeWidget};
|
||||||
utils::center,
|
|
||||||
widgets::clock::{self, ClockState, ClockStateArgs, ClockWidget, Mode as ClockMode},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// State for Countdown Widget
|
/// State for Countdown Widget
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -23,10 +31,17 @@ pub struct CountdownState {
|
|||||||
clock: ClockState<clock::Countdown>,
|
clock: ClockState<clock::Countdown>,
|
||||||
/// clock to count time after `DONE` - similar to Mission Elapsed Time (MET)
|
/// clock to count time after `DONE` - similar to Mission Elapsed Time (MET)
|
||||||
elapsed_clock: ClockState<clock::Timer>,
|
elapsed_clock: ClockState<clock::Timer>,
|
||||||
|
app_time: AppTime,
|
||||||
|
/// Edit by local time
|
||||||
|
edit_time: Option<EditTimeState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CountdownState {
|
impl CountdownState {
|
||||||
pub fn new(clock: ClockState<clock::Countdown>, elapsed_value: Duration) -> Self {
|
pub fn new(
|
||||||
|
clock: ClockState<clock::Countdown>,
|
||||||
|
elapsed_value: Duration,
|
||||||
|
app_time: AppTime,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
clock,
|
clock,
|
||||||
elapsed_clock: ClockState::<clock::Timer>::new(ClockStateArgs {
|
elapsed_clock: ClockState::<clock::Timer>::new(ClockStateArgs {
|
||||||
@ -43,6 +58,8 @@ impl CountdownState {
|
|||||||
} else {
|
} else {
|
||||||
ClockMode::Initial
|
ClockMode::Initial
|
||||||
}),
|
}),
|
||||||
|
app_time,
|
||||||
|
edit_time: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,11 +79,55 @@ impl CountdownState {
|
|||||||
pub fn get_elapsed_value(&self) -> &DurationEx {
|
pub fn get_elapsed_value(&self) -> &DurationEx {
|
||||||
self.elapsed_clock.get_current_value()
|
self.elapsed_clock.get_current_value()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_app_time(&mut self, app_time: AppTime) {
|
||||||
|
self.app_time = app_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn time_to_edit(&self) -> OffsetDateTime {
|
||||||
|
// get current value
|
||||||
|
let d: Duration = (*self.clock.get_current_value()).into();
|
||||||
|
// transform
|
||||||
|
let dd = time::Duration::try_from(d).unwrap_or(time::Duration::ZERO);
|
||||||
|
// substract from `app_time`
|
||||||
|
OffsetDateTime::from(self.app_time).saturating_add(dd)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn min_time_to_edit(&self) -> OffsetDateTime {
|
||||||
|
OffsetDateTime::from(self.app_time)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn max_time_to_edit(&self) -> OffsetDateTime {
|
||||||
|
OffsetDateTime::from(self.app_time)
|
||||||
|
.saturating_add(time::Duration::try_from(MAX_DURATION).unwrap_or(time::Duration::ZERO))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn edit_time_done(&mut self, edit_time: &mut EditTimeState) {
|
||||||
|
// get diff
|
||||||
|
let d: time::Duration = edit_time
|
||||||
|
.get_time()
|
||||||
|
.sub(OffsetDateTime::from(self.app_time));
|
||||||
|
// transfrom
|
||||||
|
let dx: DurationEx = Duration::try_from(d).unwrap_or(Duration::ZERO).into();
|
||||||
|
// update clock
|
||||||
|
self.clock.set_current_value(dx);
|
||||||
|
// remove `edit_time`
|
||||||
|
self.edit_time = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_clock_edit_mode(&self) -> bool {
|
||||||
|
self.clock.is_edit_mode()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_time_edit_mode(&self) -> bool {
|
||||||
|
self.edit_time.is_some()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventHandler for CountdownState {
|
impl EventHandler for CountdownState {
|
||||||
fn update(&mut self, event: Event) -> Option<Event> {
|
fn update(&mut self, event: Event) -> Option<Event> {
|
||||||
let edit_mode = self.clock.is_edit_mode();
|
let is_edit_clock = self.clock.is_edit_mode();
|
||||||
|
let is_edit_time = self.edit_time.is_some();
|
||||||
match event {
|
match event {
|
||||||
Event::Tick => {
|
Event::Tick => {
|
||||||
if !self.clock.is_done() {
|
if !self.clock.is_done() {
|
||||||
@ -77,12 +138,24 @@ impl EventHandler for CountdownState {
|
|||||||
self.elapsed_clock.run();
|
self.elapsed_clock.run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let min_time = self.min_time_to_edit();
|
||||||
|
let max_time = self.max_time_to_edit();
|
||||||
|
if let Some(edit_time) = &mut self.edit_time {
|
||||||
|
edit_time.set_min_time(min_time);
|
||||||
|
edit_time.set_max_time(max_time);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Event::Key(key) => match key.code {
|
Event::Key(key) => match key.code {
|
||||||
KeyCode::Char('r') => {
|
KeyCode::Char('r') => {
|
||||||
// reset both clocks
|
// reset both clocks to use intial values
|
||||||
self.clock.reset();
|
self.clock.reset();
|
||||||
self.elapsed_clock.reset();
|
self.elapsed_clock.reset();
|
||||||
|
|
||||||
|
// reset `edit_time` back initial value
|
||||||
|
let time = self.time_to_edit();
|
||||||
|
if let Some(edit_time) = &mut self.edit_time {
|
||||||
|
edit_time.set_time(time);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('s') => {
|
KeyCode::Char('s') => {
|
||||||
// toggle pause status depending on which clock is running
|
// toggle pause status depending on which clock is running
|
||||||
@ -91,30 +164,92 @@ impl EventHandler for CountdownState {
|
|||||||
} else {
|
} else {
|
||||||
self.elapsed_clock.toggle_pause();
|
self.elapsed_clock.toggle_pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// finish `edit_time` and continue for using `clock`
|
||||||
|
if let Some(edit_time) = &mut self.edit_time.clone() {
|
||||||
|
self.edit_time_done(edit_time);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('e') => {
|
// STRG + e => toggle edit time
|
||||||
self.clock.toggle_edit();
|
KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||||
// stop + reset timer entering `edit` mode
|
// stop editing clock
|
||||||
|
if self.clock.is_edit_mode() {
|
||||||
|
// toggle edit mode
|
||||||
|
self.clock.toggle_edit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(edit_time) = &mut self.edit_time.clone() {
|
||||||
|
self.edit_time_done(edit_time)
|
||||||
|
} else {
|
||||||
|
// update `edit_time`
|
||||||
|
self.edit_time = Some(EditTimeState::new(EditTimeStateArgs {
|
||||||
|
time: self.time_to_edit(),
|
||||||
|
min: self.min_time_to_edit(),
|
||||||
|
max: self.max_time_to_edit(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop `clock`
|
||||||
|
if self.clock.is_running() {
|
||||||
|
self.clock.toggle_pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop `elapsed_clock`
|
||||||
if self.elapsed_clock.is_running() {
|
if self.elapsed_clock.is_running() {
|
||||||
self.elapsed_clock.toggle_pause();
|
self.elapsed_clock.toggle_pause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Left if edit_mode => {
|
// STRG + e => toggle edit clock
|
||||||
|
KeyCode::Char('e') => {
|
||||||
|
// toggle edit mode
|
||||||
|
self.clock.toggle_edit();
|
||||||
|
|
||||||
|
// stop `elapsed_clock`
|
||||||
|
if self.elapsed_clock.is_running() {
|
||||||
|
self.elapsed_clock.toggle_pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
// finish `edit_time` and continue for using `clock`
|
||||||
|
if let Some(edit_time) = &mut self.edit_time.clone() {
|
||||||
|
self.edit_time_done(edit_time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Left if is_edit_clock => {
|
||||||
self.clock.edit_next();
|
self.clock.edit_next();
|
||||||
}
|
}
|
||||||
KeyCode::Right if edit_mode => {
|
KeyCode::Left if is_edit_time => {
|
||||||
|
// safe unwrap because of previous check in `is_edit_time`
|
||||||
|
self.edit_time.as_mut().unwrap().next();
|
||||||
|
}
|
||||||
|
KeyCode::Right if is_edit_clock => {
|
||||||
self.clock.edit_prev();
|
self.clock.edit_prev();
|
||||||
}
|
}
|
||||||
KeyCode::Up if edit_mode => {
|
KeyCode::Right if is_edit_time => {
|
||||||
|
// safe unwrap because of previous check in `is_edit_time`
|
||||||
|
self.edit_time.as_mut().unwrap().prev();
|
||||||
|
}
|
||||||
|
KeyCode::Up if is_edit_clock => {
|
||||||
self.clock.edit_up();
|
self.clock.edit_up();
|
||||||
// whenever `clock`'s value is changed, reset `elapsed_clock`
|
// whenever `clock`'s value is changed, reset `elapsed_clock`
|
||||||
self.elapsed_clock.reset();
|
self.elapsed_clock.reset();
|
||||||
}
|
}
|
||||||
KeyCode::Down if edit_mode => {
|
KeyCode::Up if is_edit_time => {
|
||||||
|
// safe unwrap because of previous check in `is_edit_time`
|
||||||
|
self.edit_time.as_mut().unwrap().up();
|
||||||
|
// whenever `clock`'s value is changed, reset `elapsed_clock`
|
||||||
|
self.elapsed_clock.reset();
|
||||||
|
}
|
||||||
|
KeyCode::Down if is_edit_clock => {
|
||||||
self.clock.edit_down();
|
self.clock.edit_down();
|
||||||
// whenever clock value is changed, reset timer
|
// whenever clock value is changed, reset timer
|
||||||
self.elapsed_clock.reset();
|
self.elapsed_clock.reset();
|
||||||
}
|
}
|
||||||
|
KeyCode::Down if is_edit_time => {
|
||||||
|
// safe unwrap because of previous check in `is_edit_time`
|
||||||
|
self.edit_time.as_mut().unwrap().down();
|
||||||
|
// whenever clock value is changed, reset timer
|
||||||
|
self.elapsed_clock.reset();
|
||||||
|
}
|
||||||
_ => return Some(event),
|
_ => return Some(event),
|
||||||
},
|
},
|
||||||
_ => return Some(event),
|
_ => return Some(event),
|
||||||
@ -127,47 +262,77 @@ pub struct Countdown {
|
|||||||
pub style: Style,
|
pub style: Style,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn human_days_diff(a: &OffsetDateTime, b: &OffsetDateTime) -> String {
|
||||||
|
let days_diff = (a.date() - b.date()).whole_days();
|
||||||
|
match days_diff {
|
||||||
|
0 => "today".to_owned(),
|
||||||
|
1 => "tomorrow".to_owned(),
|
||||||
|
n => format!("+{}days", n),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl StatefulWidget for Countdown {
|
impl StatefulWidget for Countdown {
|
||||||
type State = CountdownState;
|
type State = CountdownState;
|
||||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||||
let clock = ClockWidget::new(self.style);
|
// render `edit_time` OR `clock`
|
||||||
|
if let Some(edit_time) = &mut state.edit_time {
|
||||||
|
let label = Line::raw(
|
||||||
|
format!(
|
||||||
|
"Countdown {} {}",
|
||||||
|
edit_time.get_selected().clone(),
|
||||||
|
human_days_diff(edit_time.get_time(), &state.app_time.into())
|
||||||
|
)
|
||||||
|
.to_uppercase(),
|
||||||
|
);
|
||||||
|
let widget = EditTimeWidget::new(self.style);
|
||||||
|
let area = center(
|
||||||
|
area,
|
||||||
|
Constraint::Length(max(widget.get_width(), label.width() as u16)),
|
||||||
|
Constraint::Length(widget.get_height() + 1 /* height of label */),
|
||||||
|
);
|
||||||
|
let [v1, v2] =
|
||||||
|
Layout::vertical(Constraint::from_lengths([widget.get_height(), 1])).areas(area);
|
||||||
|
|
||||||
let label = Line::raw(
|
widget.render(v1, buf, edit_time);
|
||||||
if state.clock.is_done() {
|
label.centered().render(v2, buf);
|
||||||
if state.clock.with_decis {
|
} else {
|
||||||
format!(
|
let label = Line::raw(
|
||||||
"Countdown {} +{}",
|
if state.clock.is_done() {
|
||||||
state.clock.get_mode(),
|
if state.clock.with_decis {
|
||||||
state
|
format!(
|
||||||
.elapsed_clock
|
"Countdown {} +{}",
|
||||||
.get_current_value()
|
state.clock.get_mode(),
|
||||||
.to_string_with_decis()
|
state
|
||||||
)
|
.elapsed_clock
|
||||||
|
.get_current_value()
|
||||||
|
.to_string_with_decis()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"Countdown {} +{}",
|
||||||
|
state.clock.get_mode(),
|
||||||
|
state.elapsed_clock.get_current_value()
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
format!(
|
format!("Countdown {}", state.clock.get_mode())
|
||||||
"Countdown {} +{}",
|
|
||||||
state.clock.get_mode(),
|
|
||||||
state.elapsed_clock.get_current_value()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else {
|
.to_uppercase(),
|
||||||
format!("Countdown {}", state.clock.get_mode())
|
);
|
||||||
}
|
let widget = ClockWidget::new(self.style);
|
||||||
.to_uppercase(),
|
let area = center(
|
||||||
);
|
area,
|
||||||
|
Constraint::Length(max(
|
||||||
|
widget.get_width(&state.clock.get_format(), state.clock.with_decis),
|
||||||
|
label.width() as u16,
|
||||||
|
)),
|
||||||
|
Constraint::Length(widget.get_height() + 1 /* height of label */),
|
||||||
|
);
|
||||||
|
let [v1, v2] =
|
||||||
|
Layout::vertical(Constraint::from_lengths([widget.get_height(), 1])).areas(area);
|
||||||
|
|
||||||
let area = center(
|
widget.render(v1, buf, &mut state.clock);
|
||||||
area,
|
label.centered().render(v2, buf);
|
||||||
Constraint::Length(max(
|
}
|
||||||
clock.get_width(&state.clock.get_format(), state.clock.with_decis),
|
|
||||||
label.width() as u16,
|
|
||||||
)),
|
|
||||||
Constraint::Length(clock.get_height() + 1 /* height of label */),
|
|
||||||
);
|
|
||||||
let [v1, v2] =
|
|
||||||
Layout::vertical(Constraint::from_lengths([clock.get_height(), 1])).areas(area);
|
|
||||||
|
|
||||||
clock.render(v1, buf, &mut state.clock);
|
|
||||||
label.centered().render(v2, buf);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
231
src/widgets/edit_time.rs
Normal file
231
src/widgets/edit_time.rs
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
use std::fmt;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::{Constraint, Layout, Rect},
|
||||||
|
widgets::{StatefulWidget, Widget},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
common::Style,
|
||||||
|
widgets::clock_elements::{Colon, Digit, COLON_WIDTH, DIGIT_SPACE_WIDTH, DIGIT_WIDTH},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::clock_elements::DIGIT_HEIGHT;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Selected {
|
||||||
|
Seconds,
|
||||||
|
Minutes,
|
||||||
|
Hours,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Selected {
|
||||||
|
pub fn next(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Selected::Seconds => Selected::Minutes,
|
||||||
|
Selected::Minutes => Selected::Hours,
|
||||||
|
Selected::Hours => Selected::Seconds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Selected::Seconds => Selected::Hours,
|
||||||
|
Selected::Minutes => Selected::Seconds,
|
||||||
|
Selected::Hours => Selected::Minutes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Selected {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Selected::Seconds => write!(f, "[edit seconds]"),
|
||||||
|
Selected::Minutes => write!(f, "[edit minutes]"),
|
||||||
|
Selected::Hours => write!(f, "[edit hours]"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct EditTimeState {
|
||||||
|
selected: Selected,
|
||||||
|
time: OffsetDateTime,
|
||||||
|
min: OffsetDateTime,
|
||||||
|
max: OffsetDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct EditTimeStateArgs {
|
||||||
|
pub time: OffsetDateTime,
|
||||||
|
pub min: OffsetDateTime,
|
||||||
|
pub max: OffsetDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EditTimeState {
|
||||||
|
pub fn new(args: EditTimeStateArgs) -> Self {
|
||||||
|
EditTimeState {
|
||||||
|
time: args.time,
|
||||||
|
min: args.min,
|
||||||
|
max: args.max,
|
||||||
|
selected: Selected::Minutes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_time(&mut self, time: OffsetDateTime) {
|
||||||
|
self.time = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_min_time(&mut self, min: OffsetDateTime) {
|
||||||
|
self.min = min;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_max_time(&mut self, min: OffsetDateTime) {
|
||||||
|
self.max = min;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_time(&mut self) -> &OffsetDateTime {
|
||||||
|
&self.time
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_selected(&mut self) -> &Selected {
|
||||||
|
&self.selected
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&mut self) {
|
||||||
|
self.selected = self.selected.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev(&mut self) {
|
||||||
|
self.selected = self.selected.prev();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn up(&mut self) {
|
||||||
|
self.time = match self.selected {
|
||||||
|
Selected::Seconds => {
|
||||||
|
if self
|
||||||
|
.time
|
||||||
|
.lt(&self.max.saturating_sub(time::Duration::new(1, 0)))
|
||||||
|
{
|
||||||
|
self.time.saturating_add(time::Duration::new(1, 0))
|
||||||
|
} else {
|
||||||
|
self.time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Selected::Minutes => {
|
||||||
|
if self
|
||||||
|
.time
|
||||||
|
.lt(&self.max.saturating_sub(time::Duration::new(60, 0)))
|
||||||
|
{
|
||||||
|
self.time.saturating_add(time::Duration::new(60, 0))
|
||||||
|
} else {
|
||||||
|
self.time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Selected::Hours => {
|
||||||
|
if self
|
||||||
|
.time
|
||||||
|
.lt(&self.max.saturating_sub(time::Duration::new(60 * 60, 0)))
|
||||||
|
{
|
||||||
|
self.time.saturating_add(time::Duration::new(60 * 60, 0))
|
||||||
|
} else {
|
||||||
|
self.time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn down(&mut self) {
|
||||||
|
self.time = match self.selected {
|
||||||
|
Selected::Seconds => {
|
||||||
|
if self
|
||||||
|
.time
|
||||||
|
.ge(&self.min.saturating_add(time::Duration::new(1, 0)))
|
||||||
|
{
|
||||||
|
self.time.saturating_sub(time::Duration::new(1, 0))
|
||||||
|
} else {
|
||||||
|
self.time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Selected::Minutes => {
|
||||||
|
if self
|
||||||
|
.time
|
||||||
|
.ge(&self.min.saturating_add(time::Duration::new(60, 0)))
|
||||||
|
{
|
||||||
|
self.time.saturating_sub(time::Duration::new(60, 0))
|
||||||
|
} else {
|
||||||
|
self.time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Selected::Hours => {
|
||||||
|
if self
|
||||||
|
.time
|
||||||
|
.ge(&self.min.saturating_add(time::Duration::new(60 * 60, 0)))
|
||||||
|
{
|
||||||
|
self.time.saturating_sub(time::Duration::new(60 * 60, 0))
|
||||||
|
} else {
|
||||||
|
self.time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct EditTimeWidget {
|
||||||
|
style: Style,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EditTimeWidget {
|
||||||
|
pub fn new(style: Style) -> Self {
|
||||||
|
Self { style }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_horizontal_lengths(&self) -> Vec<u16> {
|
||||||
|
vec![
|
||||||
|
DIGIT_WIDTH, // h
|
||||||
|
DIGIT_SPACE_WIDTH, // (space)
|
||||||
|
DIGIT_WIDTH, // h
|
||||||
|
COLON_WIDTH, // :
|
||||||
|
DIGIT_WIDTH, // m
|
||||||
|
DIGIT_SPACE_WIDTH, // (space)
|
||||||
|
DIGIT_WIDTH, // m
|
||||||
|
COLON_WIDTH, // :
|
||||||
|
DIGIT_WIDTH, // s
|
||||||
|
DIGIT_SPACE_WIDTH, // (space)
|
||||||
|
DIGIT_WIDTH, // s
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_width(&self) -> u16 {
|
||||||
|
self.get_horizontal_lengths().iter().sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_height(&self) -> u16 {
|
||||||
|
DIGIT_HEIGHT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatefulWidget for EditTimeWidget {
|
||||||
|
type State = EditTimeState;
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||||
|
let symbol = self.style.get_digit_symbol();
|
||||||
|
let edit_hours = matches!(state.selected, Selected::Hours);
|
||||||
|
let edit_minutes = matches!(state.selected, Selected::Minutes);
|
||||||
|
let edit_secs = matches!(state.selected, Selected::Seconds);
|
||||||
|
|
||||||
|
let [hh, _, h, c_hm, mm, _, m, c_ms, ss, _, s] =
|
||||||
|
Layout::horizontal(Constraint::from_lengths(self.get_horizontal_lengths())).areas(area);
|
||||||
|
|
||||||
|
Digit::new((state.time.hour() as u64) / 10, edit_hours, symbol).render(hh, buf);
|
||||||
|
Digit::new((state.time.hour() as u64) % 10, edit_hours, symbol).render(h, buf);
|
||||||
|
Colon::new(symbol).render(c_hm, buf);
|
||||||
|
Digit::new((state.time.minute() as u64) / 10, edit_minutes, symbol).render(mm, buf);
|
||||||
|
Digit::new((state.time.minute() as u64) % 10, edit_minutes, symbol).render(m, buf);
|
||||||
|
Colon::new(symbol).render(c_ms, buf);
|
||||||
|
Digit::new((state.time.second() as u64) / 10, edit_secs, symbol).render(ss, buf);
|
||||||
|
Digit::new((state.time.second() as u64) % 10, edit_secs, symbol).render(s, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use crate::common::{AppTime, AppTimeFormat, Content};
|
use crate::common::{AppEditMode, AppTime, AppTimeFormat, Content};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
layout::{Constraint, Layout, Rect},
|
layout::{Constraint, Layout, Rect},
|
||||||
@ -45,7 +45,7 @@ impl FooterState {
|
|||||||
pub struct Footer {
|
pub struct Footer {
|
||||||
pub running_clock: bool,
|
pub running_clock: bool,
|
||||||
pub selected_content: Content,
|
pub selected_content: Content,
|
||||||
pub edit_mode: bool,
|
pub app_edit_mode: AppEditMode,
|
||||||
pub app_time: AppTime,
|
pub app_time: AppTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,9 +139,39 @@ impl StatefulWidget for Footer {
|
|||||||
Style::default().add_modifier(Modifier::BOLD),
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
)),
|
)),
|
||||||
Cell::from(Line::from({
|
Cell::from(Line::from({
|
||||||
if self.edit_mode {
|
match self.app_edit_mode {
|
||||||
vec![
|
AppEditMode::None => {
|
||||||
Span::from("[e]dit done"),
|
let mut spans = vec![
|
||||||
|
Span::from(if self.running_clock {
|
||||||
|
"[s]top"
|
||||||
|
} else {
|
||||||
|
"[s]tart"
|
||||||
|
}),
|
||||||
|
Span::from(SPACE),
|
||||||
|
Span::from("[r]eset"),
|
||||||
|
Span::from(SPACE),
|
||||||
|
Span::from("[e]dit"),
|
||||||
|
];
|
||||||
|
if self.selected_content == Content::Countdown {
|
||||||
|
spans.extend_from_slice(&[
|
||||||
|
Span::from(SPACE),
|
||||||
|
Span::from("[ctrl+e]dit by local time"),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if self.selected_content == Content::Pomodoro {
|
||||||
|
spans.extend_from_slice(&[
|
||||||
|
Span::from(SPACE),
|
||||||
|
Span::from("[← →]switch work/pause"),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
spans
|
||||||
|
}
|
||||||
|
others => vec![
|
||||||
|
Span::from(match others {
|
||||||
|
AppEditMode::Clock => "[e]dit done",
|
||||||
|
AppEditMode::Time => "[ctrl+e]dit done",
|
||||||
|
_ => "",
|
||||||
|
}),
|
||||||
Span::from(SPACE),
|
Span::from(SPACE),
|
||||||
Span::from(format!(
|
Span::from(format!(
|
||||||
"[{} {}]edit selection",
|
"[{} {}]edit selection",
|
||||||
@ -152,26 +182,7 @@ impl StatefulWidget for Footer {
|
|||||||
Span::from(format!("[{}]edit up", scrollbar::VERTICAL.begin)), // ↑
|
Span::from(format!("[{}]edit up", scrollbar::VERTICAL.begin)), // ↑
|
||||||
Span::from(SPACE),
|
Span::from(SPACE),
|
||||||
Span::from(format!("[{}]edit up", scrollbar::VERTICAL.end)), // ↓,
|
Span::from(format!("[{}]edit up", scrollbar::VERTICAL.end)), // ↓,
|
||||||
]
|
],
|
||||||
} else {
|
|
||||||
let mut spans = vec![
|
|
||||||
Span::from(if self.running_clock {
|
|
||||||
"[s]top"
|
|
||||||
} else {
|
|
||||||
"[s]tart"
|
|
||||||
}),
|
|
||||||
Span::from(SPACE),
|
|
||||||
Span::from("[r]eset"),
|
|
||||||
Span::from(SPACE),
|
|
||||||
Span::from("[e]dit"),
|
|
||||||
];
|
|
||||||
if self.selected_content == Content::Pomodoro {
|
|
||||||
spans.extend_from_slice(&[
|
|
||||||
Span::from(SPACE),
|
|
||||||
Span::from("[← →]switch work/pause"),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
spans
|
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
]),
|
]),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user