diff --git a/Cargo.lock b/Cargo.lock
index ce96d88..14162c6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2110,6 +2110,7 @@ dependencies = [
"tokio-util",
"tracing",
"tracing-subscriber",
+ "tui-input",
]
[[package]]
@@ -2268,6 +2269,16 @@ dependencies = [
"tracing-log",
]
+[[package]]
+name = "tui-input"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "911e93158bf80bbc94bad533b2b16e3d711e1132d69a6a6980c3920a63422c19"
+dependencies = [
+ "ratatui",
+ "unicode-width 0.2.0",
+]
+
[[package]]
name = "uds_windows"
version = "1.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index 5660ddd..d0f0bc8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -42,6 +42,7 @@ rodio = { version = "0.20.1", features = [
"symphonia-wav",
], default-features = false, optional = true }
thiserror = { version = "2.0.17", optional = true }
+tui-input = "0.14.0"
[features]
diff --git a/README.md b/README.md
index 1b21be3..c0f049a 100644
--- a/README.md
+++ b/README.md
@@ -150,6 +150,14 @@ Extra option (if `--features sound` is enabled by local build only):
| ↓ | edit to go down |
| ctrl+↓ | edit to go down 10x |
+**In `Event` `edit` mode only:**
+
+| Key | Description |
+| --- | --- |
+| Enter | save changes |
+| Esc | skip changes |
+| Tab | switch input |
+
**In `Pomodoro` screen only:**
| Key | Description |
diff --git a/src/app.rs b/src/app.rs
index be7f2b9..a8d533c 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -18,6 +18,8 @@ use crate::{
},
};
+use crossterm::event::Event as CrosstermEvent;
+
#[cfg(feature = "sound")]
use crate::sound::Sound;
@@ -25,7 +27,7 @@ use color_eyre::Result;
use ratatui::{
buffer::Buffer,
crossterm::event::{KeyCode, KeyEvent},
- layout::{Constraint, Layout, Rect},
+ layout::{Constraint, Layout, Position, Rect},
widgets::{StatefulWidget, Widget},
};
use std::path::PathBuf;
@@ -55,6 +57,7 @@ pub struct App {
style: Style,
with_decis: bool,
footer: FooterState,
+ cursor_position: Option,
}
pub struct AppArgs {
@@ -229,6 +232,7 @@ impl App {
None
},
),
+ cursor_position: None,
}
}
@@ -323,10 +327,13 @@ impl App {
Content::LocalTime => app.local_time.update(event.clone()),
} {
match unhandled {
- events::TuiEvent::Render | events::TuiEvent::Resize => {
+ events::TuiEvent::Render
+ | events::TuiEvent::Crossterm(crossterm::event::Event::Resize(_, _)) => {
app.draw(terminal)?;
}
- events::TuiEvent::Key(key) => handle_key_event(app, key),
+ events::TuiEvent::Crossterm(CrosstermEvent::Key(key)) => {
+ handle_key_event(app, key)
+ }
_ => {}
}
}
@@ -366,6 +373,9 @@ impl App {
);
}
}
+ events::AppEvent::SetCursor(position) => {
+ app.cursor_position = position;
+ }
}
Ok(())
};
@@ -411,7 +421,13 @@ impl App {
AppEditMode::None
}
}
- Content::Event => AppEditMode::None,
+ Content::Event => {
+ if self.event.is_edit_mode() {
+ AppEditMode::Event
+ } else {
+ AppEditMode::None
+ }
+ }
Content::LocalTime => AppEditMode::None,
}
}
@@ -441,6 +457,11 @@ impl App {
fn draw(&mut self, terminal: &mut Terminal) -> Result<()> {
terminal.draw(|frame| {
frame.render_stateful_widget(AppWidget, frame.area(), self);
+
+ // Set cursor position if requested
+ if let Some(position) = self.cursor_position {
+ frame.set_cursor_position(position);
+ }
})?;
Ok(())
}
diff --git a/src/common.rs b/src/common.rs
index c1b3ee7..eb1ab7f 100644
--- a/src/common.rs
+++ b/src/common.rs
@@ -209,6 +209,7 @@ pub enum AppEditMode {
None,
Clock,
Time,
+ Event,
}
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq, Default, Serialize, Deserialize)]
diff --git a/src/events.rs b/src/events.rs
index 8bc231c..b9a162e 100644
--- a/src/events.rs
+++ b/src/events.rs
@@ -1,5 +1,6 @@
-use crossterm::event::{Event as CrosstermEvent, EventStream, KeyEvent, KeyEventKind};
+use crossterm::event::{Event as CrosstermEvent, EventStream};
use futures::{Stream, StreamExt};
+use ratatui::layout::Position;
use std::{pin::Pin, time::Duration};
use tokio::sync::mpsc;
use tokio::time::interval;
@@ -20,13 +21,13 @@ pub enum TuiEvent {
Error,
Tick,
Render,
- Key(KeyEvent),
- Resize,
+ Crossterm(CrosstermEvent),
}
#[derive(Clone, Debug)]
pub enum AppEvent {
ClockDone(ClockTypeId, String),
+ SetCursor(Option),
}
pub type AppEventTx = mpsc::UnboundedSender;
@@ -89,14 +90,10 @@ fn crossterm_stream() -> Pin>> {
EventStream::new()
.fuse()
// we are not interested in all events
- .filter_map(|event| async move {
- match event {
- Ok(CrosstermEvent::Key(key)) if key.kind == KeyEventKind::Press => {
- Some(TuiEvent::Key(key))
- }
- Ok(CrosstermEvent::Resize(_, _)) => Some(TuiEvent::Resize),
+ .filter_map(|result| async move {
+ match result {
+ Ok(event) => Some(TuiEvent::Crossterm(event)),
Err(_) => Some(TuiEvent::Error),
- _ => None,
}
}),
)
diff --git a/src/widgets/countdown.rs b/src/widgets/countdown.rs
index 4d93de6..be57c2e 100644
--- a/src/widgets/countdown.rs
+++ b/src/widgets/countdown.rs
@@ -9,7 +9,7 @@ use crate::{
edit_time::{EditTimeState, EditTimeStateArgs, EditTimeWidget},
},
};
-use crossterm::event::KeyModifiers;
+use crossterm::event::{Event as CrosstermEvent, KeyModifiers};
use ratatui::{
buffer::Buffer,
crossterm::event::KeyCode,
@@ -163,102 +163,106 @@ impl TuiEventHandler for CountdownState {
}
}
// EDIT CLOCK mode
- TuiEvent::Key(key) if self.is_clock_edit_mode() => match key.code {
- // skip editing
- KeyCode::Esc => {
- // Important: set current value first
- self.clock.set_current_value(*self.clock.get_prev_value());
- // before toggling back to non-edit mode
- self.clock.toggle_edit();
- }
- // Apply changes and set new initial value
- KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) => {
- // toggle edit mode
- self.clock.toggle_edit();
- // set initial value
- self.clock
- .set_initial_value(*self.clock.get_current_value());
- // always reset `elapsed_clock`
- self.elapsed_clock.reset();
- }
- // Apply changes
- KeyCode::Char('s') => {
- // toggle edit mode
- self.clock.toggle_edit();
- // always reset `elapsed_clock`
- self.elapsed_clock.reset();
- }
- KeyCode::Right => {
- self.clock.edit_prev();
- }
- KeyCode::Left => {
- self.clock.edit_next();
- }
- KeyCode::Up if key.modifiers.contains(KeyModifiers::CONTROL) => {
- self.clock.edit_jump_up();
- }
- KeyCode::Up => {
- self.clock.edit_up();
- }
- KeyCode::Down if key.modifiers.contains(KeyModifiers::CONTROL) => {
- self.clock.edit_jump_down();
- }
- KeyCode::Down => {
- self.clock.edit_down();
- }
- _ => return Some(event),
- },
- // EDIT LOCAL TIME mode
- TuiEvent::Key(key) if self.is_time_edit_mode() => match key.code {
- // skip editing
- KeyCode::Esc => {
- self.edit_time = None;
- }
- // Apply changes and set new initial value
- KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) => {
- if let Some(edit_time) = &mut self.edit_time.clone() {
- // Order matters:
- // 1. update current value
- self.edit_time_done(edit_time);
- // 2. set initial value
+ TuiEvent::Crossterm(CrosstermEvent::Key(key)) if self.is_clock_edit_mode() => {
+ match key.code {
+ // skip editing
+ KeyCode::Esc => {
+ // Important: set current value first
+ self.clock.set_current_value(*self.clock.get_prev_value());
+ // before toggling back to non-edit mode
+ self.clock.toggle_edit();
+ }
+ // Apply changes and set new initial value
+ KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) => {
+ // toggle edit mode
+ self.clock.toggle_edit();
+ // set initial value
self.clock
.set_initial_value(*self.clock.get_current_value());
+ // always reset `elapsed_clock`
+ self.elapsed_clock.reset();
}
- // always reset `elapsed_clock`
- self.elapsed_clock.reset();
- }
- // Apply changes of editing by local time
- KeyCode::Char('s') => {
- if let Some(edit_time) = &mut self.edit_time.clone() {
- self.edit_time_done(edit_time)
+ // Apply changes
+ KeyCode::Char('s') => {
+ // toggle edit mode
+ self.clock.toggle_edit();
+ // always reset `elapsed_clock`
+ self.elapsed_clock.reset();
}
- // always reset `elapsed_clock`
- self.elapsed_clock.reset();
+ KeyCode::Right => {
+ self.clock.edit_prev();
+ }
+ KeyCode::Left => {
+ self.clock.edit_next();
+ }
+ KeyCode::Up if key.modifiers.contains(KeyModifiers::CONTROL) => {
+ self.clock.edit_jump_up();
+ }
+ KeyCode::Up => {
+ self.clock.edit_up();
+ }
+ KeyCode::Down if key.modifiers.contains(KeyModifiers::CONTROL) => {
+ self.clock.edit_jump_down();
+ }
+ KeyCode::Down => {
+ self.clock.edit_down();
+ }
+ _ => return Some(event),
}
- // move edit position to the left
- KeyCode::Left => {
- // safe unwrap because we are in `is_time_edit_mode`
- self.edit_time.as_mut().unwrap().next();
+ }
+ // EDIT LOCAL TIME mode
+ TuiEvent::Crossterm(CrosstermEvent::Key(key)) if self.is_time_edit_mode() => {
+ match key.code {
+ // skip editing
+ KeyCode::Esc => {
+ self.edit_time = None;
+ }
+ // Apply changes and set new initial value
+ KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) => {
+ if let Some(edit_time) = &mut self.edit_time.clone() {
+ // Order matters:
+ // 1. update current value
+ self.edit_time_done(edit_time);
+ // 2. set initial value
+ self.clock
+ .set_initial_value(*self.clock.get_current_value());
+ }
+ // always reset `elapsed_clock`
+ self.elapsed_clock.reset();
+ }
+ // Apply changes of editing by local time
+ KeyCode::Char('s') => {
+ if let Some(edit_time) = &mut self.edit_time.clone() {
+ self.edit_time_done(edit_time)
+ }
+ // always reset `elapsed_clock`
+ self.elapsed_clock.reset();
+ }
+ // move edit position to the left
+ KeyCode::Left => {
+ // safe unwrap because we are in `is_time_edit_mode`
+ self.edit_time.as_mut().unwrap().next();
+ }
+ // move edit position to the right
+ KeyCode::Right => {
+ // safe unwrap because we are in `is_time_edit_mode`
+ self.edit_time.as_mut().unwrap().prev();
+ }
+ // Value up
+ KeyCode::Up => {
+ // safe unwrap because of previous check in `is_time_edit_mode`
+ self.edit_time.as_mut().unwrap().up();
+ }
+ // Value down
+ KeyCode::Down => {
+ // safe unwrap because of previous check in `is_time_edit_mode`
+ self.edit_time.as_mut().unwrap().down();
+ }
+ _ => return Some(event),
}
- // move edit position to the right
- KeyCode::Right => {
- // safe unwrap because we are in `is_time_edit_mode`
- self.edit_time.as_mut().unwrap().prev();
- }
- // Value up
- KeyCode::Up => {
- // safe unwrap because of previous check in `is_time_edit_mode`
- self.edit_time.as_mut().unwrap().up();
- }
- // Value down
- KeyCode::Down => {
- // safe unwrap because of previous check in `is_time_edit_mode`
- self.edit_time.as_mut().unwrap().down();
- }
- _ => return Some(event),
- },
+ }
// default mode
- TuiEvent::Key(key) => match key.code {
+ TuiEvent::Crossterm(CrosstermEvent::Key(key)) => match key.code {
KeyCode::Char('r') => {
// reset both clocks to use intial values
self.clock.reset();
diff --git a/src/widgets/event.rs b/src/widgets/event.rs
index a7295d0..3ba4e9a 100644
--- a/src/widgets/event.rs
+++ b/src/widgets/event.rs
@@ -1,13 +1,18 @@
+use color_eyre::{Report, eyre::eyre};
+use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyModifiers};
use ratatui::{
buffer::Buffer,
- layout::{Constraint, Layout, Rect},
+ layout::{Constraint, Layout, Position, Rect},
+ style::{Color, Modifier, Style},
text::Line,
- widgets::{StatefulWidget, Widget},
+ widgets::{Paragraph, StatefulWidget, Widget},
};
use time::{OffsetDateTime, macros::format_description};
+use tui_input::Input;
+use tui_input::backend::crossterm::EventHandler;
use crate::{
- common::{AppTime, ClockTypeId, Style},
+ common::{AppTime, ClockTypeId, Style as DigitStyle},
duration::CalendarDuration,
event::Event,
events::{AppEvent, AppEventTx, TuiEvent, TuiEventHandler},
@@ -16,6 +21,44 @@ use crate::{
};
use std::{cmp::max, time::Duration};
+#[derive(Clone, Copy, Default)]
+enum Editable {
+ #[default]
+ DateTime,
+ Title,
+}
+
+impl Editable {
+ pub fn next(&self) -> Self {
+ match self {
+ Editable::DateTime => Editable::Title,
+ Editable::Title => Editable::DateTime,
+ }
+ }
+
+ pub fn prev(&self) -> Self {
+ match self {
+ Editable::DateTime => Editable::Title,
+ Editable::Title => Editable::DateTime,
+ }
+ }
+}
+
+#[derive(Clone, Copy)]
+enum EditMode {
+ None,
+ Editing(Editable),
+}
+
+impl EditMode {
+ fn is_editable(&self) -> bool {
+ match self {
+ EditMode::None => false,
+ EditMode::Editing(_) => true,
+ }
+ }
+}
+
/// State for `EventWidget`
pub struct EventState {
title: Option,
@@ -27,6 +70,13 @@ pub struct EventState {
/// Default value: `None`
done_count: Option,
app_tx: AppEventTx,
+ // inputs
+ input_datetime: Input,
+ input_datetime_error: Option,
+ input_title: Input,
+ input_title_error: Option,
+ edit_mode: EditMode,
+ last_editable: Editable,
}
pub struct EventStateArgs {
@@ -48,15 +98,23 @@ impl EventState {
let app_datetime = OffsetDateTime::from(app_time);
// assume event has as same `offset` as `app_time`
let event_offset = event.date_time.assume_offset(app_datetime.offset());
+ let input_datetime_value = format_offsetdatetime(&event_offset);
+ let input_title_value = event.title.clone().unwrap_or("".into());
Self {
- title: event.title,
+ title: event.title.clone(),
event_time: event_offset,
app_time: app_datetime,
start_time: app_datetime,
with_decis,
done_count: None,
app_tx,
+ input_datetime: Input::default().with_value(input_datetime_value),
+ input_datetime_error: None,
+ input_title: Input::default().with_value(input_title_value),
+ input_title_error: None,
+ edit_mode: EditMode::None,
+ last_editable: Editable::default(),
}
}
@@ -107,11 +165,131 @@ impl EventState {
self.done_count = clock::count_clock_done(self.done_count);
}
}
+
+ fn reset_cursor(&mut self) {
+ _ = self.app_tx.send(AppEvent::SetCursor(None));
+ }
+
+ pub fn is_edit_mode(&self) -> bool {
+ self.edit_mode.is_editable()
+ }
+
+ fn reset_edit_mode(&mut self) {
+ self.edit_mode = EditMode::None;
+ }
+
+ fn reset_input_datetime(&mut self) {
+ self.input_datetime = Input::default().with_value(format_offsetdatetime(&self.event_time));
+ self.input_datetime_error = None;
+ }
+
+ fn reset_input_title(&mut self) {
+ self.input_title = Input::default().with_value(self.title.clone().unwrap_or_default());
+ self.input_title_error = None;
+ }
+}
+
+fn validate_datetime(value: &str) -> Result {
+ time::PrimitiveDateTime::parse(
+ value,
+ format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"),
+ )
+ .map_err(|_| eyre!("Expected format 'YYYY-MM-DD HH:MM:SS'"))
+}
+
+const MAX_LABEL_WIDTH: usize = 60;
+
+fn validate_title(value: &str) -> Result<&str, Report> {
+ if value.len() > MAX_LABEL_WIDTH {
+ return Err(eyre!("Max. {} chars", MAX_LABEL_WIDTH));
+ }
+ Ok(value)
}
impl TuiEventHandler for EventState {
fn update(&mut self, event: TuiEvent) -> Option {
- Some(event)
+ let editable = self.edit_mode.is_editable();
+ match event {
+ // EDIT mode
+ TuiEvent::Crossterm(crossterm_event @ CrosstermEvent::Key(key)) if editable => {
+ match key.code {
+ // Skip changes
+ KeyCode::Esc => {
+ // reset inputs
+ self.reset_input_datetime();
+ self.reset_input_title();
+
+ self.reset_edit_mode();
+ self.reset_cursor();
+ }
+ KeyCode::Tab if key.modifiers.contains(KeyModifiers::SHIFT) => {
+ if let EditMode::Editing(e) = self.edit_mode {
+ self.last_editable = e.prev();
+ self.edit_mode = EditMode::Editing(self.last_editable)
+ }
+ }
+ KeyCode::Tab => {
+ if let EditMode::Editing(e) = self.edit_mode {
+ self.last_editable = e.next();
+ self.edit_mode = EditMode::Editing(self.last_editable)
+ }
+ }
+ KeyCode::Enter => match self.edit_mode {
+ EditMode::Editing(Editable::DateTime) => {
+ // validate
+ if let Ok(date_time) = validate_datetime(self.input_datetime.value()) {
+ // apply offset
+ self.event_time = date_time.assume_offset(self.app_time.offset());
+ } else {
+ // reset
+ self.reset_input_datetime();
+ }
+ self.reset_edit_mode();
+ self.reset_cursor();
+ }
+ EditMode::Editing(Editable::Title) => {
+ self.title = validate_title(self.input_title.value())
+ .ok()
+ .filter(|v| !v.is_empty())
+ .map(str::to_string);
+ self.reset_edit_mode();
+ self.reset_cursor();
+ }
+ EditMode::None => {}
+ },
+ _ => match self.edit_mode {
+ EditMode::Editing(Editable::DateTime) => {
+ self.input_datetime.handle_event(&crossterm_event);
+ if let Err(e) = validate_datetime(self.input_datetime.value()) {
+ self.input_datetime_error = Some(e);
+ } else {
+ self.input_datetime_error = None;
+ }
+ }
+ EditMode::Editing(Editable::Title) => {
+ self.input_title.handle_event(&crossterm_event);
+ if let Err(e) = validate_title(self.input_title.value()) {
+ self.input_title_error = Some(e);
+ } else {
+ self.input_title_error = None;
+ }
+ }
+ EditMode::None => {}
+ },
+ }
+ }
+ // default mode
+ TuiEvent::Crossterm(CrosstermEvent::Key(key)) => match key.code {
+ // Enter edit mode
+ KeyCode::Char('e') => {
+ self.edit_mode = EditMode::Editing(self.last_editable);
+ }
+ _ => return Some(event),
+ },
+ _ => return Some(event),
+ }
+
+ None
}
}
@@ -132,9 +310,16 @@ fn get_percentage(start: OffsetDateTime, end: OffsetDateTime, current: OffsetDat
percentage as u16
}
+fn format_offsetdatetime(dt: &OffsetDateTime) -> String {
+ dt.format(&format_description!(
+ "[year]-[month]-[day] [hour]:[minute]:[second]"
+ ))
+ .unwrap_or_else(|e| format!("time format error: {}", e))
+}
+
#[derive(Debug)]
pub struct EventWidget {
- pub style: Style,
+ pub style: DigitStyle,
pub blink: bool,
}
@@ -147,42 +332,20 @@ 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.clone().unwrap_or("".into()).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 = if clock_duration.is_since() {
- let duration: Duration = clock_duration.clone().into();
- // Show `done` for a short of time (1 sec)
- if duration < Duration::from_secs(1) {
- "Done"
- } else {
- "Since"
- }
- } else {
- "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_width, max_label_width)),
- Constraint::Length(DIGIT_HEIGHT + 3 /* height of label */),
+ Constraint::Length(max(clock_width, MAX_LABEL_WIDTH as u16)),
+ Constraint::Length(
+ DIGIT_HEIGHT + 7, /* height of all labels + empty lines */
+ ),
);
- 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
+ let [_, v1, v2, v3, _, v4] = Layout::vertical(Constraint::from_lengths([
+ 3, // empty (offset) to keep everything centered vertically comparing to "clock" widgets with one label only
DIGIT_HEIGHT,
- 1, // event date
- 1, // event title
+ 1, // label: event date
+ 1, // label: event title
+ 1, // empty
+ 1, // label: error
]))
.areas(area);
@@ -196,7 +359,7 @@ impl StatefulWidget for EventWidget {
let render_clock_state = clock::RenderClockState {
with_decis,
- duration: clock_duration,
+ duration: clock_duration.clone(),
editable_time: None,
format: clock_format,
symbol,
@@ -204,8 +367,120 @@ impl StatefulWidget for EventWidget {
};
clock::render_clock(v1, buf, render_clock_state);
- label_time.centered().render(v2, buf);
- label_event.centered().render(v3, buf);
+
+ // Helper to calculate centered area, cursor x position, and scroll
+ let calc_editable_input_positions = |input: &Input, area: Rect| -> (Rect, u16, usize) {
+ // Calculate scroll position to keep cursor visible
+ let input_scroll = input.visual_scroll(area.width as usize);
+
+ // Get correct visual width (handles unicode properly)
+ let text_width = Line::raw(input.value()).width() as u16;
+
+ // Calculate visible text width after scrolling
+ let visible_text_width = text_width
+ .saturating_sub(input_scroll as u16)
+ .min(area.width);
+
+ // Center the visible portion
+ let offset_x = (area.width.saturating_sub(visible_text_width)) / 2;
+
+ let centered_area = Rect {
+ x: area.x + offset_x,
+ y: area.y,
+ width: visible_text_width,
+ height: area.height,
+ };
+
+ // Cursor position relative to the visible scrolled text
+ let cursor_offset = input.visual_cursor().saturating_sub(input_scroll);
+ let cursor_x = area.x + offset_x + cursor_offset as u16;
+
+ (centered_area, cursor_x, input_scroll)
+ };
+
+ fn input_edit_style(with_error: bool) -> Style {
+ if with_error {
+ Style::default()
+ .add_modifier(Modifier::UNDERLINED)
+ .fg(Color::Red)
+ } else {
+ Style::default().add_modifier(Modifier::UNDERLINED)
+ }
+ }
+
+ // Render date time input
+ match state.edit_mode {
+ // EDIT
+ EditMode::Editing(Editable::DateTime) => {
+ let (datetime_area, datetime_cursor_x, datetime_scroll) =
+ calc_editable_input_positions(&state.input_datetime, v2);
+
+ Paragraph::new(state.input_datetime.value())
+ .style(input_edit_style(state.input_datetime_error.is_some()))
+ .scroll((0, datetime_scroll as u16))
+ .render(datetime_area, buf);
+
+ // Update cursor
+ let cp = Position::new(datetime_cursor_x, v2.y);
+ let _ = state.app_tx.send(AppEvent::SetCursor(Some(cp)));
+ }
+ // NORMAL
+ _ => {
+ let mut prefix = "Until";
+
+ if clock_duration.is_since() {
+ let duration: Duration = clock_duration.clone().into();
+ // Show `done` for a short of time (1 sec)
+ prefix = if duration < Duration::from_secs(1) {
+ "Done"
+ } else {
+ "Since"
+ };
+ };
+
+ Paragraph::new(format!(
+ "{} {}",
+ prefix.to_uppercase(),
+ state.input_datetime.value()
+ ))
+ .centered()
+ .render(v2, buf);
+ }
+ };
+
+ // Render title input
+ match state.edit_mode {
+ // EDIT
+ EditMode::Editing(Editable::Title) => {
+ let (title_area, title_cursor_x, title_scroll) =
+ calc_editable_input_positions(&state.input_title, v3);
+
+ Paragraph::new(state.input_title.value().to_uppercase())
+ .style(input_edit_style(state.input_title_error.is_some()))
+ .scroll((0, title_scroll as u16))
+ .render(title_area, buf);
+ // Update cursor
+ let cp = Position::new(title_cursor_x, v3.y);
+ let _ = state.app_tx.send(AppEvent::SetCursor(Some(cp)));
+ }
+ // NORMAL
+ _ => {
+ Paragraph::new(state.input_title.value().to_uppercase())
+ .centered()
+ .render(v3, buf);
+ }
+ };
+
+ // Render error
+ let error_txt: String = match (&state.input_datetime_error, &state.input_title_error) {
+ (Some(e), _) => e.to_string(),
+ (_, Some(e)) => e.to_string(),
+ _ => "".into(),
+ };
+ Paragraph::new(error_txt.to_lowercase())
+ .style(Style::default().add_modifier(Modifier::ITALIC))
+ .centered()
+ .render(v4, buf);
}
}
diff --git a/src/widgets/footer.rs b/src/widgets/footer.rs
index b2b0d8a..1c6e4ca 100644
--- a/src/widgets/footer.rs
+++ b/src/widgets/footer.rs
@@ -143,10 +143,8 @@ impl StatefulWidget for Footer {
]),
];
- // Controls (except for `localtime` and `event`)
- if self.selected_content != Content::LocalTime
- && self.selected_content != Content::Event
- {
+ // Controls (except for `localtime`)
+ if self.selected_content != Content::LocalTime {
table_rows.extend_from_slice(&[
// controls - 1. row
Row::new(vec![
@@ -156,7 +154,7 @@ impl StatefulWidget for Footer {
)),
Cell::from(Line::from({
match self.app_edit_mode {
- AppEditMode::None => {
+ AppEditMode::None if self.selected_content != Content::Event => {
let mut spans = vec![Span::from(if self.running_clock {
"[s]top"
} else {
@@ -184,8 +182,16 @@ impl StatefulWidget for Footer {
}
spans
}
- _ => {
+ AppEditMode::None if self.selected_content == Content::Event => {
+ vec![Span::from("[e]dit")]
+ }
+ AppEditMode::Clock | AppEditMode::Time | AppEditMode::Event => {
let mut spans = vec![Span::from("[s]ave changes")];
+
+ if self.selected_content == Content::Event {
+ spans[0] = Span::from("[enter]save changes")
+ };
+
if self.selected_content == Content::Countdown
|| self.selected_content == Content::Pomodoro
{
@@ -198,60 +204,72 @@ impl StatefulWidget for Footer {
Span::from(SPACE),
Span::from("[esc]skip changes"),
]);
+
+ if self.selected_content == Content::Event {
+ spans.extend_from_slice(&[
+ Span::from(SPACE),
+ Span::from("[tab]switch input"),
+ ]);
+ }
spans
}
+ _ => vec![],
}
})),
]),
// controls - 2. row
- Row::new(vec![
- Cell::from(Line::from("")),
- Cell::from(Line::from({
- match self.app_edit_mode {
- AppEditMode::None => {
- let mut spans = vec![];
- if self.selected_content == Content::Pomodoro {
- spans.extend_from_slice(&[Span::from(
- "[^←] or [^→] switch work/pause",
- )]);
+ Row::new(if self.selected_content == Content::Event {
+ vec![]
+ } else {
+ vec![
+ Cell::from(Line::from("")),
+ Cell::from(Line::from({
+ match self.app_edit_mode {
+ AppEditMode::None => {
+ let mut spans = vec![];
+ if self.selected_content == Content::Pomodoro {
+ spans.extend_from_slice(&[Span::from(
+ "[^←] or [^→] switch work/pause",
+ )]);
+ }
+ spans
}
- spans
+ _ => vec![
+ Span::from(format!(
+ // ← →,
+ "[{} {}]change selection",
+ scrollbar::HORIZONTAL.begin,
+ scrollbar::HORIZONTAL.end
+ )),
+ Span::from(SPACE),
+ Span::from(format!(
+ // ↑
+ "[{}]edit up",
+ scrollbar::VERTICAL.begin
+ )),
+ Span::from(SPACE),
+ Span::from(format!(
+ // ctrl + ↑
+ "[^{}]edit up 10x",
+ scrollbar::VERTICAL.begin
+ )),
+ Span::from(SPACE),
+ Span::from(format!(
+ // ↓
+ "[{}]edit up",
+ scrollbar::VERTICAL.end
+ )),
+ Span::from(SPACE),
+ Span::from(format!(
+ // ctrl + ↓
+ "[^{}]edit up 10x",
+ scrollbar::VERTICAL.end
+ )),
+ ],
}
- _ => vec![
- Span::from(format!(
- // ← →,
- "[{} {}]change selection",
- scrollbar::HORIZONTAL.begin,
- scrollbar::HORIZONTAL.end
- )),
- Span::from(SPACE),
- Span::from(format!(
- // ↑
- "[{}]edit up",
- scrollbar::VERTICAL.begin
- )),
- Span::from(SPACE),
- Span::from(format!(
- // ctrl + ↑
- "[^{}]edit up 10x",
- scrollbar::VERTICAL.begin
- )),
- Span::from(SPACE),
- Span::from(format!(
- // ↓
- "[{}]edit up",
- scrollbar::VERTICAL.end
- )),
- Span::from(SPACE),
- Span::from(format!(
- // ctrl + ↓
- "[^{}]edit up 10x",
- scrollbar::VERTICAL.end
- )),
- ],
- }
- })),
- ]),
+ })),
+ ]
+ }),
])
}
diff --git a/src/widgets/pomodoro.rs b/src/widgets/pomodoro.rs
index cb63714..3761808 100644
--- a/src/widgets/pomodoro.rs
+++ b/src/widgets/pomodoro.rs
@@ -5,7 +5,7 @@ use crate::{
utils::center,
widgets::clock::{ClockState, ClockStateArgs, ClockWidget, Countdown},
};
-use crossterm::event::{KeyCode, KeyModifiers};
+use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyModifiers};
use ratatui::{
buffer::Buffer,
layout::{Constraint, Layout, Rect},
@@ -149,7 +149,7 @@ impl TuiEventHandler for PomodoroState {
self.get_clock_mut().update_done_count();
}
// EDIT mode
- TuiEvent::Key(key) if edit_mode => match key.code {
+ TuiEvent::Crossterm(CrosstermEvent::Key(key)) if edit_mode => match key.code {
// Skip changes
KeyCode::Esc => {
let clock = self.get_clock_mut();
@@ -188,7 +188,7 @@ impl TuiEventHandler for PomodoroState {
_ => return Some(event),
},
// default mode
- TuiEvent::Key(key) => match key.code {
+ TuiEvent::Crossterm(CrosstermEvent::Key(key)) => match key.code {
// Toggle run/pause
KeyCode::Char('s') => {
self.get_clock_mut().toggle_pause();
diff --git a/src/widgets/timer.rs b/src/widgets/timer.rs
index abb48d4..a6e6412 100644
--- a/src/widgets/timer.rs
+++ b/src/widgets/timer.rs
@@ -4,7 +4,7 @@ use crate::{
utils::center,
widgets::clock::{self, ClockState, ClockWidget},
};
-use crossterm::event::KeyModifiers;
+use crossterm::event::{Event as CrosstermEvent, KeyModifiers};
use ratatui::{
buffer::Buffer,
crossterm::event::KeyCode,
@@ -41,7 +41,7 @@ impl TuiEventHandler for TimerState {
self.clock.update_done_count();
}
// EDIT mode
- TuiEvent::Key(key) if edit_mode => match key.code {
+ TuiEvent::Crossterm(CrosstermEvent::Key(key)) if edit_mode => match key.code {
// Skip changes
KeyCode::Esc => {
// Important: set current value first
@@ -78,7 +78,7 @@ impl TuiEventHandler for TimerState {
_ => return Some(event),
},
// default mode
- TuiEvent::Key(key) => match key.code {
+ TuiEvent::Crossterm(CrosstermEvent::Key(key)) => match key.code {
// Toggle run/pause
KeyCode::Char('s') => {
self.clock.toggle_pause();