standard layout (#2)
This commit is contained in:
parent
b95d7ecfb2
commit
b1e9b027a2
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,7 +2,7 @@
|
|||||||
# will have compiled files and executables
|
# will have compiled files and executables
|
||||||
debug/
|
debug/
|
||||||
target/
|
target/
|
||||||
result/**
|
result/**/*
|
||||||
|
|
||||||
# These are backup files generated by rustfmt
|
# These are backup files generated by rustfmt
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
|||||||
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -1,6 +1,6 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
@ -642,6 +642,7 @@ dependencies = [
|
|||||||
"color-eyre",
|
"color-eyre",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
|
"strum",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@ -7,3 +7,4 @@ edition = "2021"
|
|||||||
ratatui = "0.29.0"
|
ratatui = "0.29.0"
|
||||||
crossterm = "0.28.1"
|
crossterm = "0.28.1"
|
||||||
color-eyre = "0.6.2"
|
color-eyre = "0.6.2"
|
||||||
|
strum = { version = "0.26.3", features = ["derive"] }
|
||||||
|
|||||||
114
src/app.rs
Normal file
114
src/app.rs
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
use color_eyre::{eyre::Context, Result};
|
||||||
|
use crossterm::event;
|
||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind},
|
||||||
|
layout::{Constraint, Layout, Rect},
|
||||||
|
widgets::{Block, Paragraph, Widget},
|
||||||
|
DefaultTerminal, Frame,
|
||||||
|
};
|
||||||
|
use std::time::Duration;
|
||||||
|
use strum::{Display, EnumIter, FromRepr};
|
||||||
|
|
||||||
|
use crate::pomodoro::Pomodoro;
|
||||||
|
use crate::timer::Timer;
|
||||||
|
use crate::{countdown::Countdown, utils::center};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum Mode {
|
||||||
|
#[default]
|
||||||
|
Running,
|
||||||
|
Quit,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default, Display, EnumIter, FromRepr, PartialEq, Eq)]
|
||||||
|
enum Content {
|
||||||
|
#[default]
|
||||||
|
Countdown,
|
||||||
|
Timer,
|
||||||
|
Pomodoro,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct App {
|
||||||
|
content: Content,
|
||||||
|
mode: Mode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
|
while self.is_running() {
|
||||||
|
terminal
|
||||||
|
.draw(|frame| self.draw(frame))
|
||||||
|
.wrap_err("terminal.draw")?;
|
||||||
|
self.handle_events()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_running(&self) -> bool {
|
||||||
|
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_events(&mut self) -> Result<()> {
|
||||||
|
let timeout = Duration::from_secs_f64(1.0 / 50.0);
|
||||||
|
if !event::poll(timeout)? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
match event::read()? {
|
||||||
|
Event::Key(key) if key.kind == KeyEventKind::Press => self.handle_key_press(key),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_key_press(&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,
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for &App {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let vertical = Layout::vertical([
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Min(0),
|
||||||
|
Constraint::Length(1),
|
||||||
|
]);
|
||||||
|
let [header_area, content_area, footer_area] = vertical.areas(area);
|
||||||
|
|
||||||
|
Block::new().render(area, buf);
|
||||||
|
self.render_header(header_area, buf);
|
||||||
|
self.render_content(content_area, buf);
|
||||||
|
self.render_footer(footer_area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
fn render_header(&self, area: Rect, buf: &mut Buffer) {
|
||||||
|
Paragraph::new("tim:r").render(area, buf);
|
||||||
|
}
|
||||||
|
fn render_footer(&self, area: Rect, buf: &mut Buffer) {
|
||||||
|
Paragraph::new("footer").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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/countdown.rs
Normal file
24
src/countdown.rs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::Rect,
|
||||||
|
text::Text,
|
||||||
|
widgets::{Paragraph, Widget},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Countdown<'a> {
|
||||||
|
headline: Text<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Countdown<'a> {
|
||||||
|
pub const fn new(headline: Text<'a>) -> Self {
|
||||||
|
Self { headline }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for Countdown<'_> {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let h = Paragraph::new(self.headline).centered();
|
||||||
|
h.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/main.rs
72
src/main.rs
@ -1,72 +1,16 @@
|
|||||||
//! # [Ratatui] Hello World example
|
mod app;
|
||||||
//!
|
mod countdown;
|
||||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
mod pomodoro;
|
||||||
//!
|
mod timer;
|
||||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
mod utils;
|
||||||
//! repository. This means that you may not be able to compile with the latest release version on
|
|
||||||
//! crates.io, or the one that you have installed locally.
|
|
||||||
//!
|
|
||||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
|
||||||
//! library you are using.
|
|
||||||
//!
|
|
||||||
//! [Ratatui]: https://github.com/ratatui/ratatui
|
|
||||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
|
||||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
|
||||||
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
|
use app::App;
|
||||||
use color_eyre::{eyre::Context, Result};
|
use color_eyre::{eyre::Context, Result};
|
||||||
use ratatui::{
|
|
||||||
crossterm::event::{self, Event, KeyCode},
|
|
||||||
widgets::Paragraph,
|
|
||||||
DefaultTerminal, Frame,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// This is a bare minimum example. There are many approaches to running an application loop, so
|
|
||||||
/// this is not meant to be prescriptive. It is only meant to demonstrate the basic setup and
|
|
||||||
/// teardown of a terminal application.
|
|
||||||
///
|
|
||||||
/// This example does not handle events or update the application state. It just draws a greeting
|
|
||||||
/// and exits when the user presses 'q'.
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
color_eyre::install()?; // augment errors / panics with easy to read messages
|
color_eyre::install()?;
|
||||||
let terminal = ratatui::init();
|
let terminal = ratatui::init();
|
||||||
let app_result = run(terminal).context("app loop failed");
|
let app_result = App::default().run(terminal).context("app loop failed");
|
||||||
ratatui::restore();
|
ratatui::restore();
|
||||||
app_result
|
app_result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the application loop. This is where you would handle events and update the application
|
|
||||||
/// state. This example exits when the user presses 'q'. Other styles of application loops are
|
|
||||||
/// possible, for example, you could have multiple application states and switch between them based
|
|
||||||
/// on events, or you could have a single application state and update it based on events.
|
|
||||||
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
|
||||||
loop {
|
|
||||||
terminal.draw(draw)?;
|
|
||||||
if should_quit()? {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render the application. This is where you would draw the application UI. This example draws a
|
|
||||||
/// greeting.
|
|
||||||
fn draw(frame: &mut Frame) {
|
|
||||||
let greeting = Paragraph::new("Hello World! (press 'q' to quit)");
|
|
||||||
frame.render_widget(greeting, frame.area());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if the user has pressed 'q'. This is where you would handle events. This example just
|
|
||||||
/// checks if the user has pressed 'q' and returns true if they have. It does not handle any other
|
|
||||||
/// events. There is a 250ms timeout on the event poll to ensure that the terminal is rendered at
|
|
||||||
/// least once every 250ms. This allows you to do other work in the application loop, such as
|
|
||||||
/// updating the application state, without blocking the event loop for too long.
|
|
||||||
fn should_quit() -> Result<bool> {
|
|
||||||
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
|
|
||||||
if let Event::Key(key) = event::read().context("event read failed")? {
|
|
||||||
return Ok(KeyCode::Char('q') == key.code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
|
|||||||
24
src/pomodoro.rs
Normal file
24
src/pomodoro.rs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::Rect,
|
||||||
|
text::Text,
|
||||||
|
widgets::{Paragraph, Widget},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Pomodoro<'a> {
|
||||||
|
headline: Text<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Pomodoro<'a> {
|
||||||
|
pub const fn new(headline: Text<'a>) -> Self {
|
||||||
|
Self { headline }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for Pomodoro<'_> {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let h = Paragraph::new(self.headline).centered();
|
||||||
|
h.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/timer.rs
Normal file
25
src/timer.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::Rect,
|
||||||
|
text::Text,
|
||||||
|
widgets::{Paragraph, Widget},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Timer<'a> {
|
||||||
|
value: u64,
|
||||||
|
headline: Text<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Timer<'a> {
|
||||||
|
pub const fn new(value: u64, headline: Text<'a>) -> Self {
|
||||||
|
Self { value, headline }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for Timer<'_> {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let h = Paragraph::new(self.headline).centered();
|
||||||
|
h.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/utils.rs
Normal file
11
src/utils.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
use ratatui::layout::{Constraint, Flex, Layout, Rect};
|
||||||
|
|
||||||
|
/// Helper to center an area by given `Constraint`'s
|
||||||
|
/// based on [Center a Rect](https://ratatui.rs/recipes/layout/center-a-rect)
|
||||||
|
pub fn center(base_area: Rect, horizontal: Constraint, vertical: Constraint) -> Rect {
|
||||||
|
let [area] = Layout::horizontal([horizontal])
|
||||||
|
.flex(Flex::Center)
|
||||||
|
.areas(base_area);
|
||||||
|
let [area] = Layout::vertical([vertical]).flex(Flex::Center).areas(area);
|
||||||
|
area
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user