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:
parent
db5909f3d9
commit
2f587c97b5
1
.rustfmt.toml
Normal file
1
.rustfmt.toml
Normal file
@ -0,0 +1 @@
|
|||||||
|
reorder_imports = true
|
||||||
203
Cargo.lock
generated
203
Cargo.lock
generated
@ -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]]
|
||||||
|
|||||||
@ -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
14
flake.lock
generated
@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
7
justfile
7
justfile
@ -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
|
||||||
|
|||||||
121
src/app.rs
121
src/app.rs
@ -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
76
src/events.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
22
src/main.rs
22
src/main.rs
@ -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
22
src/terminal.rs
Normal 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(())
|
||||||
|
}
|
||||||
13
src/utils.rs
13
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);
|
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
5
src/widgets.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
pub mod countdown;
|
||||||
|
pub mod footer;
|
||||||
|
pub mod header;
|
||||||
|
pub mod pomodoro;
|
||||||
|
pub mod timer;
|
||||||
@ -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
32
src/widgets/header.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
@ -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);
|
||||||
Loading…
x
Reference in New Issue
Block a user