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`
This commit is contained in:
Jens K. 2024-12-02 15:43:04 +01:00 committed by GitHub
parent db5909f3d9
commit 2f587c97b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 469 additions and 95 deletions

1
.rustfmt.toml Normal file
View File

@ -0,0 +1 @@
reorder_imports = true

203
Cargo.lock generated
View File

@ -49,6 +49,15 @@ name = "bitflags"
version = "2.6.0" version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
dependencies = [
"serde",
]
[[package]]
name = "bytes"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
[[package]] [[package]]
name = "cassowary" name = "cassowary"
@ -129,9 +138,11 @@ checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"crossterm_winapi", "crossterm_winapi",
"futures-core",
"mio", "mio",
"parking_lot", "parking_lot",
"rustix", "rustix",
"serde",
"signal-hook", "signal-hook",
"signal-hook-mio", "signal-hook-mio",
"winapi", "winapi",
@ -231,6 +242,95 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" 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]] [[package]]
name = "gimli" name = "gimli"
version = "0.28.1" version = "0.28.1"
@ -434,6 +534,12 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "pretty_assertions" name = "pretty_assertions"
version = "1.4.1" version = "1.4.1"
@ -529,6 +635,26 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 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]] [[package]]
name = "sharded-slab" name = "sharded-slab"
version = "0.1.7" version = "0.1.7"
@ -574,12 +700,31 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.13.2" version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 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]] [[package]]
name = "static_assertions" name = "static_assertions"
version = "1.1.0" version = "1.1.0"
@ -641,8 +786,66 @@ version = "0.1.0"
dependencies = [ dependencies = [
"color-eyre", "color-eyre",
"crossterm", "crossterm",
"futures",
"ratatui", "ratatui",
"serde",
"strum", "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]] [[package]]

View File

@ -5,6 +5,11 @@ edition = "2021"
[dependencies] [dependencies]
ratatui = "0.29.0" ratatui = "0.29.0"
crossterm = "0.28.1" crossterm = {version = "0.28.1", features = ["event-stream", "serde"] }
color-eyre = "0.6.2" color-eyre = "0.6.2"
futures = "0.3"
serde = { version = "1", features = ["derive"] }
strum = { version = "0.26.3", 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"

14
flake.lock generated
View File

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"crane": { "crane": {
"locked": { "locked": {
"lastModified": 1732407143, "lastModified": 1733016477,
"narHash": "sha256-qJOGDT6PACoX+GbNH2PPx2ievlmtT1NVeTB80EkRLys=", "narHash": "sha256-Hh0khbqBeCtiNS0SJgqdWrQDem9WlPEc2KF5pAY+st0=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "f2b4b472983817021d9ffb60838b2b36b9376b20", "rev": "76d64e779e2fbaf172110038492343a8c4e29b55",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -56,16 +56,16 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1732521221, "lastModified": 1720535198,
"narHash": "sha256-2ThgXBUXAE1oFsVATK1ZX9IjPcS4nKFOAjhPNKuiMn0=", "narHash": "sha256-zwVvxrdIzralnSbcpghA92tWu2DV2lwv89xZc8MTrbg=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "4633a7c72337ea8fd23a4f2ba3972865e3ec685d", "rev": "205fd4226592cc83fd4c0885a3e4c9c400efabb5",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixos-unstable", "ref": "nixos-23.11",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }

View File

@ -1,6 +1,10 @@
{ {
inputs = { 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"; flake-utils.url = "github:numtide/flake-utils";
crane.url = "github:ipetkov/crane"; crane.url = "github:ipetkov/crane";
fenix = { fenix = {
@ -72,7 +76,7 @@
clippy clippy
rustfmt rustfmt
toolchain toolchain
just pkgs.just
]; ];

View File

@ -1,8 +1,9 @@
# The `--fmt` command is currently unstable. # The `--fmt` command is currently unstable.
set unstable := true # set unstable := true
default: run default:
@just --list
alias b := build alias b := build
alias f := format alias f := format
@ -20,7 +21,7 @@ test:
# format files # format files
format: format:
just --fmt # just --fmt
cargo fmt --check cargo fmt --check
# lint # lint

View File

@ -1,47 +1,70 @@
use color_eyre::{eyre::Context, Result}; use crate::{
use crossterm::event; events::{Event, Events},
terminal::Terminal,
utils::center,
widgets::{
countdown::Countdown, footer::Footer, header::Header, pomodoro::Pomodoro, timer::Timer,
},
};
use color_eyre::Result;
use ratatui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
crossterm::event::{Event, KeyCode, KeyEventKind}, crossterm::event::{KeyCode, KeyEvent},
layout::{Constraint, Layout, Rect}, layout::{Constraint, Layout, Rect},
widgets::{Block, Paragraph, Widget}, widgets::{Block, Widget},
DefaultTerminal, Frame,
}; };
use crate::footer::Footer; #[derive(Debug, Clone, Copy, PartialEq, Eq)]
use crate::pomodoro::Pomodoro;
use crate::timer::Timer;
use crate::{countdown::Countdown, utils::center};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum Mode { enum Mode {
#[default]
Running, Running,
Quit, Quit,
} }
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Content { pub enum Content {
#[default]
Countdown, Countdown,
Timer, Timer,
Pomodoro, Pomodoro,
} }
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] #[derive(Debug)]
pub struct App { pub struct App {
content: Content, content: Content,
mode: Mode, mode: Mode,
show_menu: bool, 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 { 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() { while self.is_running() {
terminal if let Some(event) = events.next().await {
.draw(|frame| self.draw(frame)) match event {
.wrap_err("terminal.draw")?; Event::Render | Event::Resize(_, _) => {
self.handle_events()?; self.draw(&mut terminal)?;
}
Event::Tick => {
self.tick = self.tick.saturating_add(1);
}
Event::Key(key) => self.handle_key_event(key),
_ => {}
}
}
} }
Ok(()) Ok(())
} }
@ -50,27 +73,33 @@ impl App {
self.mode != Mode::Quit self.mode != Mode::Quit
} }
/// Draw a single frame of the app. fn handle_key_event(&mut self, key: KeyEvent) {
fn draw(&self, frame: &mut Frame) { match key.code {
frame.render_widget(self, frame.area()); 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<()> { fn draw(&self, terminal: &mut Terminal) -> Result<()> {
if let Event::Key(key) = event::read()? { terminal.draw(|frame| {
if key.kind != KeyEventKind::Press { frame.render_widget(self, frame.area());
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,
_ => {}
};
}
Ok(()) 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 { impl Widget for &App {
@ -83,24 +112,8 @@ impl Widget for &App {
let [header_area, content_area, footer_area] = vertical.areas(area); let [header_area, content_area, footer_area] = vertical.areas(area);
Block::new().render(area, buf); 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); self.render_content(content_area, buf);
Footer::new(self.show_menu, self.content).render(footer_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),
};
}
}

76
src/events.rs Normal file
View File

@ -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<StreamKey, Pin<Box<dyn Stream<Item = Event>>>>,
}
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<Event> {
self.streams.next().await.map(|(_, event)| event)
}
}
fn tick_stream() -> Pin<Box<dyn Stream<Item = Event>>> {
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<Box<dyn Stream<Item = Event>>> {
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<dyn Stream<Item = Event>>> {
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,
}
}),
)
}

View File

@ -1,17 +1,19 @@
mod app; mod app;
mod countdown; mod events;
mod footer; mod terminal;
mod pomodoro;
mod timer;
mod utils; mod utils;
mod widgets;
use app::App; 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()?; color_eyre::install()?;
let terminal = ratatui::init(); let terminal = terminal::init()?;
let app_result = App::default().run(terminal).context("app loop failed");
ratatui::restore(); let events = events::Events::new();
app_result App::new().run(terminal, events).await?;
terminal::restore()?;
Ok(())
} }

22
src/terminal.rs Normal file
View File

@ -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<CrosstermBackend<Stdout>>;
pub fn init() -> Result<Terminal> {
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(())
}

View File

@ -9,3 +9,16 @@ pub fn center(base_area: Rect, horizontal: Constraint, vertical: Constraint) ->
let [area] = Layout::vertical([vertical]).flex(Flex::Center).areas(area); let [area] = Layout::vertical([vertical]).flex(Flex::Center).areas(area);
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)
}
}

5
src/widgets.rs Normal file
View File

@ -0,0 +1,5 @@
pub mod countdown;
pub mod footer;
pub mod header;
pub mod pomodoro;
pub mod timer;

View File

@ -1,22 +1,21 @@
use ratatui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
layout::Rect, layout::Rect,
text::Text,
widgets::{Paragraph, Widget}, widgets::{Paragraph, Widget},
}; };
#[derive(Debug, Default, Clone, PartialEq, Eq)] #[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Countdown<'a> { pub struct Countdown {
headline: Text<'a>, headline: String,
} }
impl<'a> Countdown<'a> { impl Countdown {
pub const fn new(headline: Text<'a>) -> Self { pub const fn new(headline: String) -> Self {
Self { headline } Self { headline }
} }
} }
impl Widget for Countdown<'_> { impl Widget for Countdown {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let h = Paragraph::new(self.headline).centered(); let h = Paragraph::new(self.headline).centered();
h.render(area, buf); h.render(area, buf);

32
src/widgets/header.rs Normal file
View File

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

View File

@ -1,22 +1,21 @@
use ratatui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
layout::Rect, layout::Rect,
text::Text,
widgets::{Paragraph, Widget}, widgets::{Paragraph, Widget},
}; };
#[derive(Debug, Default, Clone, PartialEq, Eq)] #[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Pomodoro<'a> { pub struct Pomodoro {
headline: Text<'a>, headline: String,
} }
impl<'a> Pomodoro<'a> { impl Pomodoro {
pub const fn new(headline: Text<'a>) -> Self { pub const fn new(headline: String) -> Self {
Self { headline } Self { headline }
} }
} }
impl Widget for Pomodoro<'_> { impl Widget for Pomodoro {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let h = Paragraph::new(self.headline).centered(); let h = Paragraph::new(self.headline).centered();
h.render(area, buf); h.render(area, buf);

View File

@ -1,23 +1,22 @@
use ratatui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
layout::Rect, layout::Rect,
text::Text,
widgets::{Paragraph, Widget}, widgets::{Paragraph, Widget},
}; };
#[derive(Debug, Default, Clone, PartialEq, Eq)] #[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Timer<'a> { pub struct Timer {
value: u64, value: u64,
headline: Text<'a>, headline: String,
} }
impl<'a> Timer<'a> { impl Timer {
pub const fn new(value: u64, headline: Text<'a>) -> Self { pub const fn new(value: u64, headline: String) -> Self {
Self { value, headline } Self { value, headline }
} }
} }
impl Widget for Timer<'_> { impl Widget for Timer {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let h = Paragraph::new(self.headline).centered(); let h = Paragraph::new(self.headline).centered();
h.render(area, buf); h.render(area, buf);