feat(footer): show local time (#42)
This commit is contained in:
53
src/app.rs
53
src/app.rs
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
args::Args,
|
||||
common::{Content, Style},
|
||||
common::{AppTime, AppTimeFormat, Content, Style},
|
||||
constants::TICK_VALUE_MS,
|
||||
events::{Event, EventHandler, Events},
|
||||
storage::AppStorage,
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
widgets::{
|
||||
clock::{self, Clock, ClockArgs},
|
||||
countdown::{Countdown, CountdownWidget},
|
||||
footer::Footer,
|
||||
footer::{Footer, FooterState},
|
||||
header::Header,
|
||||
pomodoro::{Mode as PomodoroMode, Pomodoro, PomodoroArgs, PomodoroWidget},
|
||||
timer::{Timer, TimerWidget},
|
||||
@@ -22,6 +22,7 @@ use ratatui::{
|
||||
widgets::{StatefulWidget, Widget},
|
||||
};
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::debug;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -34,18 +35,20 @@ enum Mode {
|
||||
pub struct App {
|
||||
content: Content,
|
||||
mode: Mode,
|
||||
show_menu: bool,
|
||||
app_time: AppTime,
|
||||
countdown: Countdown,
|
||||
timer: Timer,
|
||||
pomodoro: Pomodoro,
|
||||
style: Style,
|
||||
with_decis: bool,
|
||||
footer_state: FooterState,
|
||||
}
|
||||
|
||||
pub struct AppArgs {
|
||||
pub style: Style,
|
||||
pub with_decis: bool,
|
||||
pub show_menu: bool,
|
||||
pub app_time_format: AppTimeFormat,
|
||||
pub content: Content,
|
||||
pub pomodoro_mode: PomodoroMode,
|
||||
pub initial_value_work: Duration,
|
||||
@@ -64,6 +67,7 @@ impl From<(Args, AppStorage)> for AppArgs {
|
||||
AppArgs {
|
||||
with_decis: args.decis || stg.with_decis,
|
||||
show_menu: args.menu || stg.show_menu,
|
||||
app_time_format: stg.app_time_format,
|
||||
content: args.mode.unwrap_or(stg.content),
|
||||
style: args.style.unwrap_or(stg.style),
|
||||
pomodoro_mode: stg.pomodoro_mode,
|
||||
@@ -81,11 +85,19 @@ impl From<(Args, AppStorage)> for AppArgs {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_app_time() -> AppTime {
|
||||
match OffsetDateTime::now_local() {
|
||||
Ok(t) => AppTime::Local(t),
|
||||
Err(_) => AppTime::Utc(OffsetDateTime::now_utc()),
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(args: AppArgs) -> Self {
|
||||
let AppArgs {
|
||||
style,
|
||||
show_menu,
|
||||
app_time_format,
|
||||
initial_value_work,
|
||||
initial_value_pause,
|
||||
initial_value_countdown,
|
||||
@@ -100,7 +112,7 @@ impl App {
|
||||
Self {
|
||||
mode: Mode::Running,
|
||||
content,
|
||||
show_menu,
|
||||
app_time: get_app_time(),
|
||||
style,
|
||||
with_decis,
|
||||
countdown: Countdown::new(Clock::<clock::Countdown>::new(ClockArgs {
|
||||
@@ -126,12 +138,17 @@ impl App {
|
||||
style,
|
||||
with_decis,
|
||||
}),
|
||||
footer_state: FooterState::new(show_menu, app_time_format),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(mut self, mut terminal: Terminal, mut events: Events) -> Result<Self> {
|
||||
while self.is_running() {
|
||||
if let Some(event) = events.next().await {
|
||||
if matches!(event, Event::Tick) {
|
||||
self.app_time = get_app_time();
|
||||
}
|
||||
|
||||
// Pipe events into subviews and handle only 'unhandled' events afterwards
|
||||
if let Some(unhandled) = match self.content {
|
||||
Content::Countdown => self.countdown.update(event.clone()),
|
||||
@@ -186,7 +203,12 @@ impl App {
|
||||
KeyCode::Char('c') => self.content = Content::Countdown,
|
||||
KeyCode::Char('t') => self.content = Content::Timer,
|
||||
KeyCode::Char('p') => self.content = Content::Pomodoro,
|
||||
KeyCode::Char('m') => self.show_menu = !self.show_menu,
|
||||
// toogle app time format
|
||||
KeyCode::Char(':') => self.footer_state.toggle_app_time_format(),
|
||||
// toogle menu
|
||||
KeyCode::Char('m') => self
|
||||
.footer_state
|
||||
.set_show_menu(!self.footer_state.get_show_menu()),
|
||||
KeyCode::Char(',') => {
|
||||
self.style = self.style.next();
|
||||
// update clocks
|
||||
@@ -201,8 +223,8 @@ impl App {
|
||||
self.countdown.set_with_decis(self.with_decis);
|
||||
self.pomodoro.set_with_decis(self.with_decis);
|
||||
}
|
||||
KeyCode::Up => self.show_menu = true,
|
||||
KeyCode::Down => self.show_menu = false,
|
||||
KeyCode::Up => self.footer_state.set_show_menu(true),
|
||||
KeyCode::Down => self.footer_state.set_show_menu(false),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
@@ -217,7 +239,8 @@ impl App {
|
||||
pub fn to_storage(&self) -> AppStorage {
|
||||
AppStorage {
|
||||
content: self.content,
|
||||
show_menu: self.show_menu,
|
||||
show_menu: self.footer_state.get_show_menu(),
|
||||
app_time_format: *self.footer_state.app_time_format(),
|
||||
style: self.style,
|
||||
with_decis: self.with_decis,
|
||||
pomodoro_mode: self.pomodoro.get_mode().clone(),
|
||||
@@ -256,7 +279,11 @@ impl StatefulWidget for AppWidget {
|
||||
let [v0, v1, v2] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Percentage(100),
|
||||
Constraint::Length(if state.show_menu { 4 } else { 1 }),
|
||||
Constraint::Length(if state.footer_state.get_show_menu() {
|
||||
4
|
||||
} else {
|
||||
1
|
||||
}),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
@@ -268,12 +295,12 @@ impl StatefulWidget for AppWidget {
|
||||
// content
|
||||
self.render_content(v1, buf, state);
|
||||
// footer
|
||||
Footer {
|
||||
show_menu: state.show_menu,
|
||||
let footer = Footer {
|
||||
running_clock: state.clock_is_running(),
|
||||
selected_content: state.content,
|
||||
edit_mode: state.is_edit_mode(),
|
||||
}
|
||||
.render(v2, buf);
|
||||
app_time: state.app_time,
|
||||
};
|
||||
StatefulWidget::render(footer, v2, buf, &mut state.footer_state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use clap::ValueEnum;
|
||||
use ratatui::symbols::shade;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::format_description;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Default, Serialize, Deserialize,
|
||||
@@ -62,3 +64,66 @@ impl Style {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||
pub enum AppTimeFormat {
|
||||
/// `hh:mm:ss`
|
||||
#[default]
|
||||
HhMmSs,
|
||||
/// `hh:mm`
|
||||
HhMm,
|
||||
/// `hh:mm AM` (or PM)
|
||||
Hh12Mm,
|
||||
/// `` (empty)
|
||||
Hidden,
|
||||
}
|
||||
|
||||
impl AppTimeFormat {
|
||||
pub fn next(&self) -> Self {
|
||||
match self {
|
||||
AppTimeFormat::HhMmSs => AppTimeFormat::HhMm,
|
||||
AppTimeFormat::HhMm => AppTimeFormat::Hh12Mm,
|
||||
AppTimeFormat::Hh12Mm => AppTimeFormat::Hidden,
|
||||
AppTimeFormat::Hidden => AppTimeFormat::HhMmSs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum AppTime {
|
||||
Local(OffsetDateTime),
|
||||
Utc(OffsetDateTime),
|
||||
}
|
||||
|
||||
impl From<AppTime> for OffsetDateTime {
|
||||
fn from(app_time: AppTime) -> Self {
|
||||
match app_time {
|
||||
AppTime::Local(t) => t,
|
||||
AppTime::Utc(t) => t,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppTime {
|
||||
pub fn format(&self, app_format: &AppTimeFormat) -> String {
|
||||
let parse_str = match app_format {
|
||||
AppTimeFormat::HhMmSs => Some("[hour]:[minute]:[second]"),
|
||||
AppTimeFormat::HhMm => Some("[hour]:[minute]"),
|
||||
AppTimeFormat::Hh12Mm => Some("[hour]:[minute] [period]"),
|
||||
AppTimeFormat::Hidden => None,
|
||||
};
|
||||
|
||||
if let Some(str) = parse_str {
|
||||
format_description::parse(str)
|
||||
.map_err(|_| "parse error")
|
||||
.and_then(|fd| {
|
||||
OffsetDateTime::from(*self)
|
||||
.format(&fd)
|
||||
.map_err(|_| "format error")
|
||||
})
|
||||
.unwrap_or_else(|e| e.to_string())
|
||||
} else {
|
||||
"".to_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
common::{Content, Style},
|
||||
common::{AppTimeFormat, Content, Style},
|
||||
widgets::pomodoro::Mode as PomodoroMode,
|
||||
};
|
||||
use color_eyre::eyre::Result;
|
||||
@@ -12,6 +12,7 @@ use std::time::Duration;
|
||||
pub struct AppStorage {
|
||||
pub content: Content,
|
||||
pub show_menu: bool,
|
||||
pub app_time_format: AppTimeFormat,
|
||||
pub style: Style,
|
||||
pub with_decis: bool,
|
||||
pub pomodoro_mode: PomodoroMode,
|
||||
@@ -36,6 +37,7 @@ impl Default for AppStorage {
|
||||
AppStorage {
|
||||
content: Content::default(),
|
||||
show_menu: true,
|
||||
app_time_format: AppTimeFormat::default(),
|
||||
style: Style::default(),
|
||||
with_decis: false,
|
||||
pomodoro_mode: PomodoroMode::Work,
|
||||
|
||||
@@ -1,25 +1,57 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::common::Content;
|
||||
use crate::common::{AppTime, AppTimeFormat, Content};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Modifier, Style},
|
||||
symbols::{border, scrollbar},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Cell, Row, Table, Widget},
|
||||
widgets::{Block, Borders, Cell, Row, StatefulWidget, Table, Widget},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FooterState {
|
||||
show_menu: bool,
|
||||
app_time_format: AppTimeFormat,
|
||||
}
|
||||
|
||||
impl FooterState {
|
||||
pub const fn new(show_menu: bool, app_time_format: AppTimeFormat) -> Self {
|
||||
Self {
|
||||
show_menu,
|
||||
app_time_format,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_show_menu(&mut self, value: bool) {
|
||||
self.show_menu = value;
|
||||
}
|
||||
|
||||
pub const fn get_show_menu(&self) -> bool {
|
||||
self.show_menu
|
||||
}
|
||||
|
||||
pub const fn app_time_format(&self) -> &AppTimeFormat {
|
||||
&self.app_time_format
|
||||
}
|
||||
|
||||
pub fn toggle_app_time_format(&mut self) {
|
||||
self.app_time_format = self.app_time_format.next();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Footer {
|
||||
pub show_menu: bool,
|
||||
pub running_clock: bool,
|
||||
pub selected_content: Content,
|
||||
pub edit_mode: bool,
|
||||
pub app_time: AppTime,
|
||||
}
|
||||
|
||||
impl Widget for Footer {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
impl StatefulWidget for Footer {
|
||||
type State = FooterState;
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
let content_labels: BTreeMap<Content, &str> = BTreeMap::from([
|
||||
(Content::Countdown, "[c]ountdown"),
|
||||
(Content::Timer, "[t]imer"),
|
||||
@@ -31,15 +63,25 @@ impl Widget for Footer {
|
||||
|
||||
let [border_area, menu_area] =
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Percentage(100)]).areas(area);
|
||||
|
||||
Block::new()
|
||||
.borders(Borders::TOP)
|
||||
.title(
|
||||
format! {"[m]enu {:} ", if self.show_menu {scrollbar::VERTICAL.end} else {scrollbar::VERTICAL.begin}},
|
||||
format! {"[m]enu {:} ", if state.show_menu {scrollbar::VERTICAL.end} else {scrollbar::VERTICAL.begin}},
|
||||
)
|
||||
.title(
|
||||
Line::from(
|
||||
match state.app_time_format {
|
||||
// `Hidden` -> no (empty) title
|
||||
AppTimeFormat::Hidden => "".into(),
|
||||
// others -> add some space around
|
||||
_ => format!(" {} ", self.app_time.format(&state.app_time_format))
|
||||
}
|
||||
).right_aligned())
|
||||
.border_set(border::PLAIN)
|
||||
.render(border_area, buf);
|
||||
// show menu
|
||||
if self.show_menu {
|
||||
if state.show_menu {
|
||||
let content_labels: Vec<Span> = content_labels
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -60,7 +102,7 @@ impl Widget for Footer {
|
||||
|
||||
const SPACE: &str = " "; // 2 empty spaces
|
||||
let widths = [Constraint::Length(12), Constraint::Percentage(100)];
|
||||
Table::new(
|
||||
let table = Table::new(
|
||||
[
|
||||
// content
|
||||
Row::new(vec![
|
||||
@@ -80,6 +122,14 @@ impl Widget for Footer {
|
||||
Span::from("[,]change style"),
|
||||
Span::from(SPACE),
|
||||
Span::from("[.]toggle deciseconds"),
|
||||
Span::from(SPACE),
|
||||
Span::from(format!(
|
||||
"[:]toggle {} time",
|
||||
match self.app_time {
|
||||
AppTime::Local(_) => "local",
|
||||
AppTime::Utc(_) => "utc",
|
||||
}
|
||||
)),
|
||||
])),
|
||||
]),
|
||||
// edit
|
||||
@@ -128,8 +178,9 @@ impl Widget for Footer {
|
||||
],
|
||||
widths,
|
||||
)
|
||||
.column_spacing(1)
|
||||
.render(menu_area, buf);
|
||||
.column_spacing(1);
|
||||
|
||||
Widget::render(table, menu_area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user