Edit countdown by local time (#49)

This commit is contained in:
Jens Krause 2025-01-13 18:44:56 +01:00 committed by GitHub
parent b1efb1eb62
commit 6d2bf5ac09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 572 additions and 132 deletions

View File

@ -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);

View File

@ -128,6 +128,13 @@ impl AppTime {
} }
} }
#[derive(Debug)]
pub enum AppEditMode {
None,
Clock,
Time,
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {

View File

@ -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,

View File

@ -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;

View File

@ -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,
@ -499,15 +495,15 @@ where
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,
@ -517,11 +513,11 @@ where
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,
@ -529,11 +525,11 @@ where
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,
@ -543,7 +539,7 @@ where
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,
@ -551,7 +547,7 @@ where
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,

View File

@ -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] = [

View File

@ -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
KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => {
// stop editing clock
if self.clock.is_edit_mode() {
// toggle edit mode
self.clock.toggle_edit(); self.clock.toggle_edit();
// stop + reset timer entering `edit` mode }
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,11 +262,40 @@ 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);
widget.render(v1, buf, edit_time);
label.centered().render(v2, buf);
} else {
let label = Line::raw( let label = Line::raw(
if state.clock.is_done() { if state.clock.is_done() {
if state.clock.with_decis { if state.clock.with_decis {
@ -155,19 +319,20 @@ impl StatefulWidget for Countdown {
} }
.to_uppercase(), .to_uppercase(),
); );
let widget = ClockWidget::new(self.style);
let area = center( let area = center(
area, area,
Constraint::Length(max( Constraint::Length(max(
clock.get_width(&state.clock.get_format(), state.clock.with_decis), widget.get_width(&state.clock.get_format(), state.clock.with_decis),
label.width() as u16, label.width() as u16,
)), )),
Constraint::Length(clock.get_height() + 1 /* height of label */), Constraint::Length(widget.get_height() + 1 /* height of label */),
); );
let [v1, v2] = let [v1, v2] =
Layout::vertical(Constraint::from_lengths([clock.get_height(), 1])).areas(area); Layout::vertical(Constraint::from_lengths([widget.get_height(), 1])).areas(area);
clock.render(v1, buf, &mut state.clock); widget.render(v1, buf, &mut state.clock);
label.centered().render(v2, buf); label.centered().render(v2, buf);
} }
} }
}

231
src/widgets/edit_time.rs Normal file
View 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);
}
}

View File

@ -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,21 +139,8 @@ 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"),
Span::from(SPACE),
Span::from(format!(
"[{} {}]edit selection",
scrollbar::HORIZONTAL.begin,
scrollbar::HORIZONTAL.end
)), // ← →,
Span::from(SPACE),
Span::from(format!("[{}]edit up", scrollbar::VERTICAL.begin)), // ↑
Span::from(SPACE),
Span::from(format!("[{}]edit up", scrollbar::VERTICAL.end)), // ↓,
]
} else {
let mut spans = vec![ let mut spans = vec![
Span::from(if self.running_clock { Span::from(if self.running_clock {
"[s]top" "[s]top"
@ -165,6 +152,12 @@ impl StatefulWidget for Footer {
Span::from(SPACE), Span::from(SPACE),
Span::from("[e]dit"), 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 { if self.selected_content == Content::Pomodoro {
spans.extend_from_slice(&[ spans.extend_from_slice(&[
Span::from(SPACE), Span::from(SPACE),
@ -173,6 +166,24 @@ impl StatefulWidget for Footer {
} }
spans spans
} }
others => vec![
Span::from(match others {
AppEditMode::Clock => "[e]dit done",
AppEditMode::Time => "[ctrl+e]dit done",
_ => "",
}),
Span::from(SPACE),
Span::from(format!(
"[{} {}]edit selection",
scrollbar::HORIZONTAL.begin,
scrollbar::HORIZONTAL.end
)), // ← →,
Span::from(SPACE),
Span::from(format!("[{}]edit up", scrollbar::VERTICAL.begin)), // ↑
Span::from(SPACE),
Span::from(format!("[{}]edit up", scrollbar::VERTICAL.end)), // ↓,
],
}
})), })),
]), ]),
], ],