Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

### Fix

- (fix) remove `Render` event handling entirely [#158](https://github.com/sectore/timr-tui/pull/158)
- (perf) reduce CPU usage by implementing conditional redraws [#157](https://github.com/sectore/timr-tui/pull/157) by @fgbm


## v1.7.0 - 2026-02-02

### Features
Expand Down
60 changes: 30 additions & 30 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ pub struct App {
with_decis: bool,
footer: FooterState,
cursor_position: Option<Position>,
needs_redraw: bool,
}

pub struct AppArgs {
Expand Down Expand Up @@ -240,7 +239,6 @@ impl App {
},
),
cursor_position: None,
needs_redraw: true,
}
}

Expand All @@ -252,7 +250,6 @@ impl App {
// Closure to handle `KeyEvent`'s
let handle_key_event = |app: &mut Self, key: KeyEvent| {
debug!("Received key {:?}", key.code);
app.needs_redraw = true;
match key.code {
KeyCode::Char('q') => app.mode = Mode::Quit,
KeyCode::Char('1') | KeyCode::Char('c') /* TODO: deprecated, remove it in next major version */ => app.content = Content::Countdown,
Expand Down Expand Up @@ -319,46 +316,39 @@ impl App {
};
};
// Closure to handle `TuiEvent`'s
let mut handle_tui_events = |app: &mut Self, event: events::TuiEvent| -> Result<()> {
let handle_tui_events = |app: &mut Self, event: events::TuiEvent| -> Result<bool> {
if matches!(event, events::TuiEvent::Tick) {
app.app_time = AppTime::new();
app.countdown.set_app_time(app.app_time);
app.local_time.set_app_time(app.app_time);
app.event.set_app_time(app.app_time);
app.needs_redraw = true;
}

// Pipe events into subviews and handle only 'unhandled' events afterwards
if let Some(unhandled) = match app.content {
let unhandled = match app.content {
Content::Countdown => app.countdown.update(event.clone()),
Content::Timer => app.timer.update(event.clone()),
Content::Pomodoro => app.pomodoro.update(event.clone()),
Content::Event => app.event.update(event.clone()),
Content::LocalTime => app.local_time.update(event.clone()),
} {
match unhandled {
events::TuiEvent::Render => {
if app.needs_redraw {
app.draw(terminal)?;
app.needs_redraw = false;
}
}
events::TuiEvent::Crossterm(crossterm::event::Event::Resize(_, _)) => {
app.draw(terminal)?;
app.needs_redraw = false;
}
events::TuiEvent::Crossterm(CrosstermEvent::Key(key)) => {
handle_key_event(app, key)
}
_ => {}
}
};
if let Some(events::TuiEvent::Crossterm(CrosstermEvent::Key(key))) = unhandled {
handle_key_event(app, key);
}
Ok(())

// Trigger re-draw for specific events only.
let trigger_redraw = matches!(
event,
events::TuiEvent::Tick
| events::TuiEvent::Crossterm(CrosstermEvent::Key(_))
| events::TuiEvent::Crossterm(CrosstermEvent::Resize(_, _))
);
Ok(trigger_redraw)
};

// Closure to handle `AppEvent`'s
let handle_app_events = |app: &mut Self, event: events::AppEvent| -> Result<()> {
app.needs_redraw = true;
let handle_app_events = |app: &mut Self, event: events::AppEvent| -> Result<bool> {
let mut trigger_redraw = false;
match event {
events::AppEvent::ClockDone(type_id, name) => {
debug!("AppEvent::ClockDone");
Expand Down Expand Up @@ -388,16 +378,26 @@ impl App {
}
events::AppEvent::SetCursor(position) => {
app.cursor_position = position;
// Trigger re-draw by setting cursor smoothly
trigger_redraw = true;
}
}
Ok(())
Ok(trigger_redraw)
};

while self.is_running() {
if let Some(event) = events.next().await {
let _ = match event {
events::Event::Terminal(e) => handle_tui_events(&mut self, e),
events::Event::App(e) => handle_app_events(&mut self, e),
match event {
events::Event::Terminal(e) => {
if let Ok(true) = handle_tui_events(&mut self, e) {
self.draw(terminal)?;
}
}
events::Event::App(e) => {
if let Ok(true) = handle_app_events(&mut self, e) {
self.draw(terminal)?;
}
}
};
}
}
Expand Down
1 change: 0 additions & 1 deletion src/constants.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
pub static APP_NAME: &str = env!("CARGO_PKG_NAME");

pub static TICK_VALUE_MS: u64 = 1000 / 10; // 0.1 sec in milliseconds
pub static FPS_VALUE_MS: u64 = 1000 / 60; // 60 FPS in milliseconds
10 changes: 1 addition & 9 deletions src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,18 @@ use tokio::time::interval;
use tokio_stream::{StreamMap, wrappers::IntervalStream};

use crate::common::ClockTypeId;
use crate::constants::{FPS_VALUE_MS, TICK_VALUE_MS};
use crate::constants::TICK_VALUE_MS;

#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
enum StreamKey {
Ticks,
Render,
Crossterm,
}

#[derive(Clone, Debug)]
pub enum TuiEvent {
Error,
Tick,
Render,
Crossterm(CrosstermEvent),
}

Expand All @@ -43,7 +41,6 @@ impl Default for Events {
Self {
streams: StreamMap::from_iter([
(StreamKey::Ticks, tick_stream()),
(StreamKey::Render, render_stream()),
(StreamKey::Crossterm, crossterm_stream()),
]),
app_channel: mpsc::unbounded_channel(),
Expand Down Expand Up @@ -80,11 +77,6 @@ fn tick_stream() -> Pin<Box<dyn Stream<Item = TuiEvent>>> {
Box::pin(IntervalStream::new(tick_interval).map(|_| TuiEvent::Tick))
}

fn render_stream() -> Pin<Box<dyn Stream<Item = TuiEvent>>> {
let render_interval = interval(Duration::from_millis(FPS_VALUE_MS));
Box::pin(IntervalStream::new(render_interval).map(|_| TuiEvent::Render))
}

fn crossterm_stream() -> Pin<Box<dyn Stream<Item = TuiEvent>>> {
Box::pin(
EventStream::new()
Expand Down