feat(event) make date_time + title editable (#130)

* wip: editable events

* make it work

* fix: scroll position, title validation, underline

inputs to visualize edit mode

* show error

* prefix datetime

* refactor rendering inputs

* compact `EditMode`

* update footer to include `event` keybindings

* update README
This commit is contained in:
Jens Krause 2025-10-15 16:49:17 +02:00 committed by GitHub
parent e11dcaa913
commit 95d914c757
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 539 additions and 203 deletions

11
Cargo.lock generated
View File

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

View File

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

View File

@ -150,6 +150,14 @@ Extra option (if `--features sound` is enabled by local build only):
| <kbd></kbd> | edit to go down |
| <kbd>ctrl+↓</kbd> | edit to go down 10x |
**In `Event` `edit` mode only:**
| Key | Description |
| --- | --- |
| <kbd>Enter</kbd> | save changes |
| <kbd>Esc</kbd> | skip changes |
| <kbd>Tab</kbd> | switch input |
**In `Pomodoro` screen only:**
| Key | Description |

View File

@ -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<Position>,
}
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(())
}

View File

@ -209,6 +209,7 @@ pub enum AppEditMode {
None,
Clock,
Time,
Event,
}
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq, Default, Serialize, Deserialize)]

View File

@ -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<Position>),
}
pub type AppEventTx = mpsc::UnboundedSender<AppEvent>;
@ -89,14 +90,10 @@ fn crossterm_stream() -> Pin<Box<dyn Stream<Item = TuiEvent>>> {
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,
}
}),
)

View File

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

View File

@ -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<String>,
@ -27,6 +70,13 @@ pub struct EventState {
/// Default value: `None`
done_count: Option<u64>,
app_tx: AppEventTx,
// inputs
input_datetime: Input,
input_datetime_error: Option<Report>,
input_title: Input,
input_title_error: Option<Report>,
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, Report> {
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<TuiEvent> {
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);
}
}

View File

@ -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
)),
],
}
})),
]),
})),
]
}),
])
}

View File

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

View File

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