feat(cli) parse event (#125)
* feat(cli) parse `event` * check possible `Event` for `mode` * m.bros
This commit is contained in:
parent
758a72fbf6
commit
56e6ce66fa
@ -90,6 +90,7 @@ Options:
|
||||
-c, --countdown <COUNTDOWN> Countdown time to start from. Formats: 'Yy Dd hh:mm:ss', 'Dd hh:mm:ss', 'Yy mm:ss', 'Dd mm:ss', 'Yy ss', 'Dd ss', 'hh:mm:ss', 'mm:ss', 'ss'. Examples: '1y 5d 10:30:00', '2d 4:00', '1d 10', '5:03'.
|
||||
-w, --work <WORK> Work time to count down from. Formats: 'ss', 'mm:ss', 'hh:mm:ss'
|
||||
-p, --pause <PAUSE> Pause time to count down from. Formats: 'ss', 'mm:ss', 'hh:mm:ss'
|
||||
-e, --event <EVENT> Event date time and title (optional). Format: 'YYYY-MM-DD HH:MM:SS' or 'time=YYYY-MM-DD HH:MM:SS[,title=...]'. Examples: '2025-10-10 14:30:00' or 'time=2025-10-10 14:30:00,title=My Event'.
|
||||
-d, --decis Show deciseconds.
|
||||
-m, --mode <MODE> Mode to start with. [possible values: countdown, timer, pomodoro, event, localtime]
|
||||
-s, --style <STYLE> Style to display time with. [possible values: full, light, medium, dark, thick, cross, braille]
|
||||
|
||||
14
src/app.rs
14
src/app.rs
@ -2,6 +2,7 @@ use crate::{
|
||||
args::Args,
|
||||
common::{AppEditMode, AppTime, AppTimeFormat, ClockTypeId, Content, Style, Toggle},
|
||||
constants::TICK_VALUE_MS,
|
||||
event::{Event, get_default_event},
|
||||
events::{self, TuiEventHandler},
|
||||
storage::AppStorage,
|
||||
terminal::Terminal,
|
||||
@ -29,7 +30,6 @@ 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)]
|
||||
@ -75,6 +75,7 @@ pub struct AppArgs {
|
||||
pub current_value_countdown: Duration,
|
||||
pub elapsed_value_countdown: Duration,
|
||||
pub current_value_timer: Duration,
|
||||
pub event: Event,
|
||||
pub app_tx: events::AppEventTx,
|
||||
pub sound_path: Option<PathBuf>,
|
||||
pub footer_toggle_app_time: Toggle,
|
||||
@ -107,6 +108,8 @@ impl From<FromAppArgs> for App {
|
||||
Content::Pomodoro
|
||||
} else if args.countdown.is_some() {
|
||||
Content::Countdown
|
||||
} else if args.event.is_some() {
|
||||
Content::Event
|
||||
}
|
||||
// in other case just use latest stored state
|
||||
else {
|
||||
@ -132,6 +135,7 @@ impl From<FromAppArgs> for App {
|
||||
None => stg.elapsed_value_countdown,
|
||||
},
|
||||
current_value_timer: stg.current_value_timer,
|
||||
event: args.event.unwrap_or_else(get_default_event),
|
||||
app_tx,
|
||||
#[cfg(feature = "sound")]
|
||||
sound_path: args.sound,
|
||||
@ -160,6 +164,7 @@ impl App {
|
||||
with_decis,
|
||||
pomodoro_mode,
|
||||
pomodoro_round,
|
||||
event,
|
||||
notification,
|
||||
blink,
|
||||
sound_path,
|
||||
@ -212,12 +217,7 @@ impl App {
|
||||
}),
|
||||
event: EventState::new(EventStateArgs {
|
||||
app_time,
|
||||
event_time: time::PrimitiveDateTime::parse(
|
||||
"2025-10-10 10:30:30",
|
||||
format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"),
|
||||
)
|
||||
.unwrap(),
|
||||
event_title: "My event".to_owned(),
|
||||
event,
|
||||
with_decis,
|
||||
app_tx: app_tx.clone(),
|
||||
}),
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
common::{Content, Style, Toggle},
|
||||
duration,
|
||||
event::{Event, parse_event},
|
||||
};
|
||||
#[cfg(feature = "sound")]
|
||||
use crate::{sound, sound::SoundError};
|
||||
@ -28,6 +29,14 @@ pub struct Args {
|
||||
)]
|
||||
pub pause: Option<Duration>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
short = 'e',
|
||||
value_parser = parse_event,
|
||||
help = "Event date time and title (optional). Format: 'YYYY-MM-DD HH:MM:SS' or 'time=YYYY-MM-DD HH:MM:SS[,title=...]'. Examples: '2025-10-10 14:30:00' or 'time=2025-10-10 14:30:00,title=My Event'."
|
||||
)]
|
||||
pub event: Option<Event>,
|
||||
|
||||
#[arg(long, short = 'd', help = "Show deciseconds.")]
|
||||
pub decis: bool,
|
||||
|
||||
|
||||
170
src/event.rs
Normal file
170
src/event.rs
Normal file
@ -0,0 +1,170 @@
|
||||
use time::macros::format_description;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Event {
|
||||
pub date_time: time::PrimitiveDateTime,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
pub fn get_default_event() -> Event {
|
||||
Event {
|
||||
date_time: time::PrimitiveDateTime::parse(
|
||||
// Mario Bros "...entered mass production in Japan on June 21" 1983
|
||||
// https://en.wikipedia.org/wiki/Mario_Bros.#Release
|
||||
"1983-06-21 00:00",
|
||||
format_description!("[year]-[month]-[day] [hour]:[minute]"),
|
||||
)
|
||||
.unwrap(),
|
||||
title: Some("Release date of Mario Bros in Japan".into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses an `Event`
|
||||
/// Supports two formats:
|
||||
/// (1) "YYYY-MM-DD HH:MM:SS"
|
||||
/// (2) "time=YYYY-MM-DD HH:MM:SS,title=my event"
|
||||
pub fn parse_event(s: &str) -> Result<Event, String> {
|
||||
let s = s.trim();
|
||||
|
||||
// check + parse (2)
|
||||
if s.contains('=') {
|
||||
parse_event_key_value(s)
|
||||
} else {
|
||||
// parse (1)
|
||||
parse_event_date_time(s)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses an `Event` based on "YYYY-MM-DD HH:MM:SS" format
|
||||
fn parse_event_date_time(s: &str) -> Result<Event, String> {
|
||||
let time = time::PrimitiveDateTime::parse(
|
||||
s,
|
||||
format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"),
|
||||
)
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"Failed to parse event date_time '{}': {}. Expected format: 'YYYY-MM-DD HH:MM:SS'",
|
||||
s, e
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Event {
|
||||
date_time: time,
|
||||
title: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parses an `Event` defined by a `key=value` pair.
|
||||
/// Valid keys: `time` and `title`.
|
||||
/// Format: "time=YYYY-MM-DD HH:MM:SS,title=my event"
|
||||
fn parse_event_key_value(s: &str) -> Result<Event, String> {
|
||||
let mut time_str = None;
|
||||
let mut title_str = None;
|
||||
|
||||
// k/v pairs are splitted by commas
|
||||
for part in s.split(',') {
|
||||
let part = part.trim();
|
||||
if let Some((key, value)) = part.split_once('=') {
|
||||
match key.trim() {
|
||||
"time" => time_str = Some(value.trim()),
|
||||
"title" => title_str = Some(value.trim()),
|
||||
unknown => {
|
||||
return Err(format!(
|
||||
"Unknown key '{}'. Valid keys: 'time', 'title'",
|
||||
unknown
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(format!(
|
||||
"Invalid key=value pair: '{}'. Expected format: 'key=value'",
|
||||
part
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let time_str = time_str.ok_or(
|
||||
"Missing required 'time' field. Expected format: 'time=YYYY-MM-DD HH:MM:SS[,title=...]'",
|
||||
)?;
|
||||
let time = time::PrimitiveDateTime::parse(
|
||||
time_str,
|
||||
format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"),
|
||||
)
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"Failed to parse event time '{}': {}. Expected format: 'YYYY-MM-DD HH:MM:SS'",
|
||||
time_str, e
|
||||
)
|
||||
})?;
|
||||
|
||||
let title = title_str.filter(|t| !t.is_empty()).map(|t| t.to_string());
|
||||
|
||||
Ok(Event {
|
||||
date_time: time,
|
||||
title,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use time::macros::datetime;
|
||||
|
||||
#[test]
|
||||
fn test_parse_event() {
|
||||
// Simple format: time only
|
||||
let result = parse_event("2024-01-01 14:30:00").unwrap();
|
||||
assert_eq!(result.date_time, datetime!(2024-01-01 14:30:00));
|
||||
assert_eq!(result.title, None);
|
||||
|
||||
// Simple format: with leading/trailing whitespace (outer trim works)
|
||||
let result = parse_event(" 2025-12-25 12:30:00 ").unwrap();
|
||||
assert_eq!(result.date_time, datetime!(2025-12-25 12:30:00));
|
||||
assert_eq!(result.title, None);
|
||||
|
||||
// Key=value format: time only
|
||||
let result = parse_event("time=2025-10-10 14:30:00").unwrap();
|
||||
assert_eq!(result.date_time, datetime!(2025-10-10 14:30:00));
|
||||
assert_eq!(result.title, None);
|
||||
|
||||
// Key=value format: time and title
|
||||
let result = parse_event("time=2025-10-10 14:30:00,title=Team Meeting").unwrap();
|
||||
assert_eq!(result.date_time, datetime!(2025-10-10 14:30:00));
|
||||
assert_eq!(result.title, Some("Team Meeting".to_string()));
|
||||
|
||||
// Key=value format: order independent
|
||||
let result = parse_event("title=Stand-up,time=2025-10-10 09:00:00").unwrap();
|
||||
assert_eq!(result.date_time, datetime!(2025-10-10 09:00:00));
|
||||
assert_eq!(result.title, Some("Stand-up".to_string()));
|
||||
|
||||
// Key=value format: title with spaces and special chars
|
||||
let result =
|
||||
parse_event("time=2025-10-10 14:30:00,title=Sprint Planning: Q1 Review").unwrap();
|
||||
assert_eq!(result.date_time, datetime!(2025-10-10 14:30:00));
|
||||
assert_eq!(result.title, Some("Sprint Planning: Q1 Review".to_string()));
|
||||
|
||||
// Key=value format: empty title treated as None
|
||||
let result = parse_event("time=2025-10-10 14:30:00,title=").unwrap();
|
||||
assert_eq!(result.date_time, datetime!(2025-10-10 14:30:00));
|
||||
assert_eq!(result.title, None);
|
||||
|
||||
// Key=value format: whitespace handling
|
||||
let result = parse_event(" time = 2025-10-10 14:30:00 , title = My Event ").unwrap();
|
||||
assert_eq!(result.date_time, datetime!(2025-10-10 14:30:00));
|
||||
assert_eq!(result.title, Some("My Event".to_string()));
|
||||
|
||||
// Error cases: invalid time format
|
||||
assert!(parse_event("2025-13-01 00:00:00").is_err());
|
||||
assert!(parse_event("invalid").is_err());
|
||||
assert!(parse_event("2025/10/10 14:30:00").is_err());
|
||||
|
||||
// Error cases: missing time in key=value format
|
||||
assert!(parse_event("title=My Event").is_err());
|
||||
|
||||
// Error cases: unknown key
|
||||
assert!(parse_event("time=2025-10-10 14:30:00,foo=bar").is_err());
|
||||
|
||||
// Error cases: malformed key=value pair
|
||||
assert!(parse_event("time=2025-10-10 14:30:00,notapair").is_err());
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@ mod app;
|
||||
mod common;
|
||||
mod config;
|
||||
mod constants;
|
||||
mod event;
|
||||
mod events;
|
||||
mod logging;
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ use time::{OffsetDateTime, macros::format_description};
|
||||
use crate::{
|
||||
common::{AppTime, ClockTypeId, Style},
|
||||
duration::CalendarDuration,
|
||||
event::Event,
|
||||
events::{AppEvent, AppEventTx, TuiEvent, TuiEventHandler},
|
||||
utils::center,
|
||||
widgets::{clock, clock_elements::DIGIT_HEIGHT},
|
||||
@ -17,7 +18,7 @@ use std::{cmp::max, time::Duration};
|
||||
|
||||
/// State for `EventWidget`
|
||||
pub struct EventState {
|
||||
title: String,
|
||||
title: Option<String>,
|
||||
event_time: OffsetDateTime,
|
||||
app_time: OffsetDateTime,
|
||||
start_time: OffsetDateTime,
|
||||
@ -30,8 +31,7 @@ pub struct EventState {
|
||||
|
||||
pub struct EventStateArgs {
|
||||
pub app_time: AppTime,
|
||||
pub event_time: time::PrimitiveDateTime,
|
||||
pub event_title: String,
|
||||
pub event: Event,
|
||||
pub with_decis: bool,
|
||||
pub app_tx: AppEventTx,
|
||||
}
|
||||
@ -40,18 +40,17 @@ impl EventState {
|
||||
pub fn new(args: EventStateArgs) -> Self {
|
||||
let EventStateArgs {
|
||||
app_time,
|
||||
event_time,
|
||||
event_title,
|
||||
event,
|
||||
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 event_offset = event.date_time.assume_offset(app_datetime.offset());
|
||||
|
||||
Self {
|
||||
title: event_title,
|
||||
title: event.title,
|
||||
event_time: event_offset,
|
||||
app_time: app_datetime,
|
||||
start_time: app_datetime,
|
||||
@ -92,9 +91,10 @@ impl EventState {
|
||||
// reset `done_count`
|
||||
self.done_count = Some(clock::MAX_DONE_COUNT);
|
||||
// send notification
|
||||
_ = self
|
||||
.app_tx
|
||||
.send(AppEvent::ClockDone(ClockTypeId::Event, self.title.clone()));
|
||||
_ = self.app_tx.send(AppEvent::ClockDone(
|
||||
ClockTypeId::Event,
|
||||
self.title.clone().unwrap_or("".into()),
|
||||
));
|
||||
}
|
||||
// count (possible) `done`
|
||||
self.done_count = clock::count_clock_done(self.done_count);
|
||||
@ -140,7 +140,7 @@ impl StatefulWidget for EventWidget {
|
||||
let clock_widths = clock::clock_horizontal_lengths(&clock_format, with_decis);
|
||||
let clock_width = clock_widths.iter().sum();
|
||||
|
||||
let label_event = Line::raw(state.title.to_uppercase());
|
||||
let label_event = Line::raw(state.title.clone().unwrap_or("".into()).to_uppercase());
|
||||
let time_str = state
|
||||
.event_time
|
||||
.format(&format_description!(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user