From 2f587c97b5450ab1d56baa158cb24950237c62a2 Mon Sep 17 00:00:00 2001 From: "Jens K." <47693+sectore@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:43:04 +0100 Subject: [PATCH] Event handling (#5) - Refactor `event` handling (heavily inspired by [crates-tui](https://github.com/ratatui/crates-tui/) via [Tui with Terminal and EventHandler](https://ratatui.rs/recipes/apps/terminal-and-event-handler/)) - Refactor widget structure - Disable `nixos-unstable` temporarily - Add `.rustfmt.toml` --- .rustfmt.toml | 1 + Cargo.lock | 203 +++++++++++++++++++++++++++++++++ Cargo.toml | 7 +- flake.lock | 14 +-- flake.nix | 8 +- justfile | 7 +- src/app.rs | 121 +++++++++++--------- src/events.rs | 76 ++++++++++++ src/main.rs | 22 ++-- src/terminal.rs | 22 ++++ src/utils.rs | 13 +++ src/widgets.rs | 5 + src/{ => widgets}/countdown.rs | 11 +- src/{ => widgets}/footer.rs | 0 src/widgets/header.rs | 32 ++++++ src/{ => widgets}/pomodoro.rs | 11 +- src/{ => widgets}/timer.rs | 11 +- 17 files changed, 469 insertions(+), 95 deletions(-) create mode 100644 .rustfmt.toml create mode 100644 src/events.rs create mode 100644 src/terminal.rs create mode 100644 src/widgets.rs rename src/{ => widgets}/countdown.rs (64%) rename src/{ => widgets}/footer.rs (100%) create mode 100644 src/widgets/header.rs rename src/{ => widgets}/pomodoro.rs (65%) rename src/{ => widgets}/timer.rs (66%) diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..44148a2 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +reorder_imports = true diff --git a/Cargo.lock b/Cargo.lock index eb7d9db..4a38f2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,6 +49,15 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] + +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cassowary" @@ -129,9 +138,11 @@ checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ "bitflags", "crossterm_winapi", + "futures-core", "mio", "parking_lot", "rustix", + "serde", "signal-hook", "signal-hook-mio", "winapi", @@ -231,6 +242,95 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "gimli" version = "0.28.1" @@ -434,6 +534,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pretty_assertions" version = "1.4.1" @@ -529,6 +635,26 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "serde" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -574,12 +700,31 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -641,8 +786,66 @@ version = "0.1.0" dependencies = [ "color-eyre", "crossterm", + "futures", "ratatui", + "serde", "strum", + "tokio", + "tokio-stream", + "tokio-util", +] + +[[package]] +name = "tokio" +version = "1.41.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f8fb0c8..f5b6602 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,11 @@ edition = "2021" [dependencies] ratatui = "0.29.0" -crossterm = "0.28.1" +crossterm = {version = "0.28.1", features = ["event-stream", "serde"] } color-eyre = "0.6.2" +futures = "0.3" +serde = { version = "1", features = ["derive"] } strum = { version = "0.26.3", features = ["derive"] } +tokio = { version = "1.41.1", features = ["full"] } +tokio-stream = "0.1.16" +tokio-util = "0.7.12" diff --git a/flake.lock b/flake.lock index fb125d8..0370627 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "crane": { "locked": { - "lastModified": 1732407143, - "narHash": "sha256-qJOGDT6PACoX+GbNH2PPx2ievlmtT1NVeTB80EkRLys=", + "lastModified": 1733016477, + "narHash": "sha256-Hh0khbqBeCtiNS0SJgqdWrQDem9WlPEc2KF5pAY+st0=", "owner": "ipetkov", "repo": "crane", - "rev": "f2b4b472983817021d9ffb60838b2b36b9376b20", + "rev": "76d64e779e2fbaf172110038492343a8c4e29b55", "type": "github" }, "original": { @@ -56,16 +56,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1732521221, - "narHash": "sha256-2ThgXBUXAE1oFsVATK1ZX9IjPcS4nKFOAjhPNKuiMn0=", + "lastModified": 1720535198, + "narHash": "sha256-zwVvxrdIzralnSbcpghA92tWu2DV2lwv89xZc8MTrbg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "4633a7c72337ea8fd23a4f2ba3972865e3ec685d", + "rev": "205fd4226592cc83fd4c0885a3e4c9c400efabb5", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-unstable", + "ref": "nixos-23.11", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index 9907d2b..fba3cff 100644 --- a/flake.nix +++ b/flake.nix @@ -1,6 +1,10 @@ { inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + # Disable `nixos-unstable` for now, it introduced some `VScode` related errors: + # error: function 'buildVscodeExtension' called without required argument 'pname' + # nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11"; + # nixpkgs.url = "github:NixOS/nixpkgs/a8a983027ca02b363dfc82fbe3f7d9548a8d3dce"; flake-utils.url = "github:numtide/flake-utils"; crane.url = "github:ipetkov/crane"; fenix = { @@ -72,7 +76,7 @@ clippy rustfmt toolchain - just + pkgs.just ]; diff --git a/justfile b/justfile index b5f52d1..01951cc 100644 --- a/justfile +++ b/justfile @@ -1,8 +1,9 @@ # The `--fmt` command is currently unstable. -set unstable := true +# set unstable := true -default: run +default: + @just --list alias b := build alias f := format @@ -20,7 +21,7 @@ test: # format files format: - just --fmt + # just --fmt cargo fmt --check # lint diff --git a/src/app.rs b/src/app.rs index 9d1a01c..2380ad0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,47 +1,70 @@ -use color_eyre::{eyre::Context, Result}; -use crossterm::event; +use crate::{ + events::{Event, Events}, + terminal::Terminal, + utils::center, + widgets::{ + countdown::Countdown, footer::Footer, header::Header, pomodoro::Pomodoro, timer::Timer, + }, +}; +use color_eyre::Result; use ratatui::{ buffer::Buffer, - crossterm::event::{Event, KeyCode, KeyEventKind}, + crossterm::event::{KeyCode, KeyEvent}, layout::{Constraint, Layout, Rect}, - widgets::{Block, Paragraph, Widget}, - DefaultTerminal, Frame, + widgets::{Block, Widget}, }; -use crate::footer::Footer; -use crate::pomodoro::Pomodoro; -use crate::timer::Timer; -use crate::{countdown::Countdown, utils::center}; - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Mode { - #[default] Running, Quit, } -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum Content { - #[default] Countdown, Timer, Pomodoro, } -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[derive(Debug)] pub struct App { content: Content, mode: Mode, show_menu: bool, + tick: u128, +} + +impl Default for App { + fn default() -> Self { + Self { + mode: Mode::Running, + content: Content::Countdown, + show_menu: false, + tick: 0, + } + } } impl App { - pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { + pub fn new() -> Self { + Self::default() + } + + pub async fn run(&mut self, mut terminal: Terminal, mut events: Events) -> Result<()> { while self.is_running() { - terminal - .draw(|frame| self.draw(frame)) - .wrap_err("terminal.draw")?; - self.handle_events()?; + if let Some(event) = events.next().await { + match event { + Event::Render | Event::Resize(_, _) => { + self.draw(&mut terminal)?; + } + Event::Tick => { + self.tick = self.tick.saturating_add(1); + } + Event::Key(key) => self.handle_key_event(key), + _ => {} + } + } } Ok(()) } @@ -50,27 +73,33 @@ impl App { self.mode != Mode::Quit } - /// Draw a single frame of the app. - fn draw(&self, frame: &mut Frame) { - frame.render_widget(self, frame.area()); + fn handle_key_event(&mut self, key: KeyEvent) { + match key.code { + KeyCode::Char('q') | KeyCode::Esc => self.mode = Mode::Quit, + KeyCode::Char('c') => self.content = Content::Countdown, + KeyCode::Char('t') => self.content = Content::Timer, + KeyCode::Char('p') => self.content = Content::Pomodoro, + KeyCode::Char('m') => self.show_menu = !self.show_menu, + _ => {} + }; } - fn handle_events(&mut self) -> Result<()> { - if let Event::Key(key) = event::read()? { - if key.kind != KeyEventKind::Press { - return Ok(()); - } - match key.code { - KeyCode::Char('q') | KeyCode::Esc => self.mode = Mode::Quit, - KeyCode::Char('c') => self.content = Content::Countdown, - KeyCode::Char('t') => self.content = Content::Timer, - KeyCode::Char('p') => self.content = Content::Pomodoro, - KeyCode::Char('m') => self.show_menu = !self.show_menu, - _ => {} - }; - } + fn draw(&self, terminal: &mut Terminal) -> Result<()> { + terminal.draw(|frame| { + frame.render_widget(self, frame.area()); + })?; Ok(()) } + + fn render_content(&self, area: Rect, buf: &mut Buffer) { + // center content + let area = center(area, Constraint::Length(50), Constraint::Length(1)); + match self.content { + Content::Timer => Timer::new(200, "Timer".into()).render(area, buf), + Content::Countdown => Countdown::new("Countdown".into()).render(area, buf), + Content::Pomodoro => Pomodoro::new("Pomodoro".into()).render(area, buf), + }; + } } impl Widget for &App { @@ -83,24 +112,8 @@ impl Widget for &App { let [header_area, content_area, footer_area] = vertical.areas(area); Block::new().render(area, buf); - self.render_header(header_area, buf); + Header::new(self.tick).render(header_area, buf); self.render_content(content_area, buf); Footer::new(self.show_menu, self.content).render(footer_area, buf); } } - -impl App { - fn render_header(&self, area: Rect, buf: &mut Buffer) { - Paragraph::new("tim:r").render(area, buf); - } - - fn render_content(&self, area: Rect, buf: &mut Buffer) { - // center content - let area = center(area, Constraint::Length(50), Constraint::Length(1)); - match self.content { - Content::Timer => Timer::new(200, "Timer".into()).render(area, buf), - Content::Countdown => Countdown::new("Countdown".into()).render(area, buf), - Content::Pomodoro => Pomodoro::new("Pomodoro".into()).render(area, buf), - }; - } -} diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..30d8135 --- /dev/null +++ b/src/events.rs @@ -0,0 +1,76 @@ +use crossterm::event::{Event as CrosstermEvent, EventStream, KeyEvent, KeyEventKind}; +use futures::{Stream, StreamExt}; +use std::{pin::Pin, time::Duration}; +use tokio::time::interval; +use tokio_stream::{wrappers::IntervalStream, StreamMap}; + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +enum StreamKey { + Ticks, + Render, + Crossterm, +} +#[derive(Clone, Debug)] +pub enum Event { + Init, + Quit, + Error, + Tick, + Render, + Key(KeyEvent), + Resize(u16, u16), +} + +pub struct Events { + streams: StreamMap>>>, +} + +impl Default for Events { + fn default() -> Self { + Self { + streams: StreamMap::from_iter([ + (StreamKey::Ticks, tick_stream()), + (StreamKey::Render, render_stream()), + (StreamKey::Crossterm, crossterm_stream()), + ]), + } + } +} + +impl Events { + pub fn new() -> Self { + Self::default() + } + + pub async fn next(&mut self) -> Option { + self.streams.next().await.map(|(_, event)| event) + } +} + +fn tick_stream() -> Pin>> { + let tick_interval = interval(Duration::from_secs_f64(1.0 / 10.0)); + Box::pin(IntervalStream::new(tick_interval).map(|_| Event::Tick)) +} + +fn render_stream() -> Pin>> { + let render_interval = interval(Duration::from_secs_f64(1.0 / 60.0)); // 60 FPS + Box::pin(IntervalStream::new(render_interval).map(|_| Event::Render)) +} + +fn crossterm_stream() -> Pin>> { + Box::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(Event::Key(key)) + } + Ok(CrosstermEvent::Resize(x, y)) => Some(Event::Resize(x, y)), + Err(_) => Some(Event::Error), + _ => None, + } + }), + ) +} diff --git a/src/main.rs b/src/main.rs index 5d6a649..e2c4bb4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,19 @@ mod app; -mod countdown; -mod footer; -mod pomodoro; -mod timer; +mod events; +mod terminal; mod utils; +mod widgets; use app::App; -use color_eyre::{eyre::Context, Result}; +use color_eyre::Result; -fn main() -> Result<()> { +#[tokio::main] +async fn main() -> Result<()> { color_eyre::install()?; - let terminal = ratatui::init(); - let app_result = App::default().run(terminal).context("app loop failed"); - ratatui::restore(); - app_result + let terminal = terminal::init()?; + + let events = events::Events::new(); + App::new().run(terminal, events).await?; + terminal::restore()?; + Ok(()) } diff --git a/src/terminal.rs b/src/terminal.rs new file mode 100644 index 0000000..bdc68b5 --- /dev/null +++ b/src/terminal.rs @@ -0,0 +1,22 @@ +use std::io::{stdout, Stdout}; + +use color_eyre::eyre::Result; +use crossterm::{execute, terminal::*}; +use ratatui::{backend::CrosstermBackend, Terminal as RatatuiTerminal}; + +pub type Terminal = RatatuiTerminal>; + +pub fn init() -> Result { + enable_raw_mode()?; + execute!(stdout(), EnterAlternateScreen)?; + let mut terminal = RatatuiTerminal::new(CrosstermBackend::new(stdout()))?; + terminal.clear()?; + terminal.hide_cursor()?; + Ok(terminal) +} + +pub fn restore() -> Result<()> { + execute!(stdout(), LeaveAlternateScreen)?; + disable_raw_mode()?; + Ok(()) +} diff --git a/src/utils.rs b/src/utils.rs index 8f6b8db..518c64d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -9,3 +9,16 @@ pub fn center(base_area: Rect, horizontal: Constraint, vertical: Constraint) -> let [area] = Layout::vertical([vertical]).flex(Flex::Center).areas(area); area } + +pub fn format_ms(ms: u128, show_tenths: bool) -> String { + // let hours = ms / 3600000; + let minutes = (ms % 3600000) / 60000; + let seconds = (ms % 60000) / 1000; + let tenths = (ms % 1000) / 100; + + if show_tenths { + format!("{:02}:{:02}.{}", minutes, seconds, tenths) + } else { + format!("{:02}:{:02}", minutes, seconds) + } +} diff --git a/src/widgets.rs b/src/widgets.rs new file mode 100644 index 0000000..fbae324 --- /dev/null +++ b/src/widgets.rs @@ -0,0 +1,5 @@ +pub mod countdown; +pub mod footer; +pub mod header; +pub mod pomodoro; +pub mod timer; diff --git a/src/countdown.rs b/src/widgets/countdown.rs similarity index 64% rename from src/countdown.rs rename to src/widgets/countdown.rs index 5a9c752..2b0a165 100644 --- a/src/countdown.rs +++ b/src/widgets/countdown.rs @@ -1,22 +1,21 @@ use ratatui::{ buffer::Buffer, layout::Rect, - text::Text, widgets::{Paragraph, Widget}, }; #[derive(Debug, Default, Clone, PartialEq, Eq)] -pub struct Countdown<'a> { - headline: Text<'a>, +pub struct Countdown { + headline: String, } -impl<'a> Countdown<'a> { - pub const fn new(headline: Text<'a>) -> Self { +impl Countdown { + pub const fn new(headline: String) -> Self { Self { headline } } } -impl Widget for Countdown<'_> { +impl Widget for Countdown { fn render(self, area: Rect, buf: &mut Buffer) { let h = Paragraph::new(self.headline).centered(); h.render(area, buf); diff --git a/src/footer.rs b/src/widgets/footer.rs similarity index 100% rename from src/footer.rs rename to src/widgets/footer.rs diff --git a/src/widgets/header.rs b/src/widgets/header.rs new file mode 100644 index 0000000..c03fd6d --- /dev/null +++ b/src/widgets/header.rs @@ -0,0 +1,32 @@ +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Layout, Rect}, + text::Span, + widgets::Widget, +}; + +use crate::utils::format_ms; + +#[derive(Debug, Clone)] +pub struct Header { + tick: u128, +} + +impl Header { + pub fn new(tick: u128) -> Self { + Self { tick } + } +} + +impl Widget for Header { + fn render(self, area: Rect, buf: &mut Buffer) { + let time_string = format_ms(self.tick * 100, true); + let tick_span = Span::raw(time_string); + let tick_width = tick_span.width().try_into().unwrap_or(0); + let [h1, h2] = + Layout::horizontal([Constraint::Fill(1), Constraint::Length(tick_width)]).areas(area); + + Span::raw("tim:r").render(h1, buf); + tick_span.render(h2, buf); + } +} diff --git a/src/pomodoro.rs b/src/widgets/pomodoro.rs similarity index 65% rename from src/pomodoro.rs rename to src/widgets/pomodoro.rs index d6c5dfa..919c72e 100644 --- a/src/pomodoro.rs +++ b/src/widgets/pomodoro.rs @@ -1,22 +1,21 @@ use ratatui::{ buffer::Buffer, layout::Rect, - text::Text, widgets::{Paragraph, Widget}, }; #[derive(Debug, Default, Clone, PartialEq, Eq)] -pub struct Pomodoro<'a> { - headline: Text<'a>, +pub struct Pomodoro { + headline: String, } -impl<'a> Pomodoro<'a> { - pub const fn new(headline: Text<'a>) -> Self { +impl Pomodoro { + pub const fn new(headline: String) -> Self { Self { headline } } } -impl Widget for Pomodoro<'_> { +impl Widget for Pomodoro { fn render(self, area: Rect, buf: &mut Buffer) { let h = Paragraph::new(self.headline).centered(); h.render(area, buf); diff --git a/src/timer.rs b/src/widgets/timer.rs similarity index 66% rename from src/timer.rs rename to src/widgets/timer.rs index 12a9962..ccd7f64 100644 --- a/src/timer.rs +++ b/src/widgets/timer.rs @@ -1,23 +1,22 @@ use ratatui::{ buffer::Buffer, layout::Rect, - text::Text, widgets::{Paragraph, Widget}, }; #[derive(Debug, Default, Clone, PartialEq, Eq)] -pub struct Timer<'a> { +pub struct Timer { value: u64, - headline: Text<'a>, + headline: String, } -impl<'a> Timer<'a> { - pub const fn new(value: u64, headline: Text<'a>) -> Self { +impl Timer { + pub const fn new(value: u64, headline: String) -> Self { Self { value, headline } } } -impl Widget for Timer<'_> { +impl Widget for Timer { fn render(self, area: Rect, buf: &mut Buffer) { let h = Paragraph::new(self.headline).centered(); h.render(area, buf);