feat: add basic ratatui event loop and print time to screen

This commit is contained in:
Stevan Freeborn
2026-07-22 19:57:29 -05:00
parent ef9b18c173
commit 011639d0d1
+104 -9
View File
@@ -1,6 +1,26 @@
use std::{
io::{self, stdout},
sync::mpsc,
thread,
time::Duration,
};
use crossterm::{
ExecutableCommand,
event::{self, Event, KeyCode, KeyModifiers},
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{
Terminal,
backend::CrosstermBackend,
layout::Alignment,
style::{Color, Style},
widgets::Paragraph,
};
const SECONDS_PER_MIN: i64 = 60; const SECONDS_PER_MIN: i64 = 60;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Clone)]
enum TimerState { enum TimerState {
Work, Work,
Break, Break,
@@ -30,6 +50,17 @@ impl Termato {
} }
} }
fn toggle_running(&mut self) {
if self.is_running {
self.prior_state = self.state.clone();
self.state = TimerState::Paused;
self.is_running = false;
} else {
self.state = self.prior_state.clone();
self.is_running = true;
}
}
fn toggle_mode(&mut self) { fn toggle_mode(&mut self) {
let is_on_break = self.state == TimerState::Break let is_on_break = self.state == TimerState::Break
|| (self.state == TimerState::Paused && self.prior_state == TimerState::Break); || (self.state == TimerState::Paused && self.prior_state == TimerState::Break);
@@ -58,17 +89,58 @@ impl Termato {
} }
} }
fn main() { fn main() -> Result<(), io::Error> {
let mut termato = Termato::new(1, 1); enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let mut terminal = Terminal::new(backend)?;
let mut termato = Termato::new(1, 1);
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
loop { loop {
termato.is_running = true; thread::sleep(Duration::from_secs(1));
if tx.send(()).is_err() {
termato.tick(); break;
}
println!("Time remaining: {}", termato.time_remaining_in_sec);
println!("State: {:?}", termato.state);
} }
});
loop {
terminal.draw(|f| {
let size = f.area();
let minutes = termato.time_remaining_in_sec / SECONDS_PER_MIN;
let seconds = termato.time_remaining_in_sec % SECONDS_PER_MIN;
let time_str = format!("{:02}:{:02}", minutes, seconds);
let paragraph = Paragraph::new(time_str)
.style(Style::default().fg(Color::White))
.alignment(Alignment::Center);
f.render_widget(paragraph, size);
})?;
if event::poll(Duration::from_millis(100))?
&& let Event::Key(key) = event::read()?
&& key.kind == event::KeyEventKind::Press
{
match key.code {
KeyCode::Char('q') | KeyCode::Char('Q') => break,
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => break,
KeyCode::Char(' ') => termato.toggle_running(),
_ => {}
}
}
if rx.try_recv().is_ok() {
termato.tick();
}
}
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
} }
#[cfg(test)] #[cfg(test)]
@@ -160,4 +232,27 @@ mod tests {
assert_eq!(termato.duration_in_secs, 120); assert_eq!(termato.duration_in_secs, 120);
assert_eq!(termato.time_remaining_in_sec, 120); assert_eq!(termato.time_remaining_in_sec, 120);
} }
#[test]
fn termato_toggle_running_when_called_and_already_running_it_should_pause() {
let mut termato = Termato::new(1, 1);
termato.is_running = true;
termato.toggle_running();
assert_eq!(termato.state, TimerState::Paused);
assert_eq!(termato.prior_state, TimerState::Work);
assert!(!termato.is_running)
}
#[test]
fn termato_toggle_running_when_called_and_already_paused_it_should_run() {
let mut termato = Termato::new(1, 1);
termato.toggle_running();
assert_eq!(termato.state, TimerState::Work);
assert_eq!(termato.prior_state, TimerState::Work);
assert!(termato.is_running)
}
} }