add: realm boilerplate, copy of demo

This commit is contained in:
Charles
2024-11-26 22:24:13 -08:00
parent e1c1ed0995
commit 6076d13a70
9 changed files with 1589 additions and 2 deletions
+7
View File
@@ -0,0 +1,7 @@
//! ## Terminal
//!
//! terminal helper
pub use super::*;
pub mod model;
+190
View File
@@ -0,0 +1,190 @@
//! ## Model
//!
//! app model
use std::time::{Duration, SystemTime};
use tuirealm::event::NoUserEvent;
use tuirealm::props::{Alignment, Color, TextModifiers};
use tuirealm::ratatui::layout::{Constraint, Direction, Layout};
use tuirealm::terminal::{CrosstermTerminalAdapter, TerminalAdapter, TerminalBridge};
use tuirealm::{
Application, AttrValue, Attribute, EventListenerCfg, Sub, SubClause, SubEventClause, Update,
};
use super::components::{Clock, DigitCounter, Label, LetterCounter};
use super::{Id, Msg};
pub struct Model<T>
where
T: TerminalAdapter,
{
/// Application
pub app: Application<Id, Msg, NoUserEvent>,
/// Indicates that the application must quit
pub quit: bool,
/// Tells whether to redraw interface
pub redraw: bool,
/// Used to draw to terminal
pub terminal: TerminalBridge<T>,
}
impl Default for Model<CrosstermTerminalAdapter> {
fn default() -> Self {
Self {
app: Self::init_app(),
quit: false,
redraw: true,
terminal: TerminalBridge::init_crossterm().expect("Cannot initialize terminal"),
}
}
}
impl<T> Model<T>
where
T: TerminalAdapter,
{
pub fn view(&mut self) {
assert!(self
.terminal
.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints(
[
Constraint::Length(3), // Clock
Constraint::Length(3), // Letter Counter
Constraint::Length(3), // Digit Counter
Constraint::Length(1), // Label
]
.as_ref(),
)
.split(f.area());
self.app.view(&Id::Clock, f, chunks[0]);
self.app.view(&Id::LetterCounter, f, chunks[1]);
self.app.view(&Id::DigitCounter, f, chunks[2]);
self.app.view(&Id::Label, f, chunks[3]);
})
.is_ok());
}
fn init_app() -> Application<Id, Msg, NoUserEvent> {
// Setup application
// NOTE: NoUserEvent is a shorthand to tell tui-realm we're not going to use any custom user event
// NOTE: the event listener is configured to use the default crossterm input listener and to raise a Tick event each second
// which we will use to update the clock
let mut app: Application<Id, Msg, NoUserEvent> = Application::init(
EventListenerCfg::default()
.crossterm_input_listener(Duration::from_millis(20), 3)
.poll_timeout(Duration::from_millis(10))
.tick_interval(Duration::from_secs(1)),
);
// Mount components
assert!(app
.mount(
Id::Label,
Box::new(
Label::default()
.text("Waiting for a Msg...")
.alignment(Alignment::Left)
.background(Color::Reset)
.foreground(Color::LightYellow)
.modifiers(TextModifiers::BOLD),
),
Vec::default(),
)
.is_ok());
// Mount clock, subscribe to tick
assert!(app
.mount(
Id::Clock,
Box::new(
Clock::new(SystemTime::now())
.alignment(Alignment::Center)
.background(Color::Reset)
.foreground(Color::Cyan)
.modifiers(TextModifiers::BOLD)
),
vec![Sub::new(SubEventClause::Tick, SubClause::Always)]
)
.is_ok());
// Mount counters
assert!(app
.mount(
Id::LetterCounter,
Box::new(LetterCounter::new(0)),
Vec::new()
)
.is_ok());
assert!(app
.mount(
Id::DigitCounter,
Box::new(DigitCounter::new(5)),
Vec::default()
)
.is_ok());
// Active letter counter
assert!(app.active(&Id::LetterCounter).is_ok());
app
}
}
// Let's implement Update for model
impl<T> Update<Msg> for Model<T>
where
T: TerminalAdapter,
{
fn update(&mut self, msg: Option<Msg>) -> Option<Msg> {
if let Some(msg) = msg {
// Set redraw
self.redraw = true;
// Match message
match msg {
Msg::AppClose => {
self.quit = true; // Terminate
None
}
Msg::Clock => None,
Msg::DigitCounterBlur => {
// Give focus to letter counter
assert!(self.app.active(&Id::LetterCounter).is_ok());
None
}
Msg::DigitCounterChanged(v) => {
// Update label
assert!(self
.app
.attr(
&Id::Label,
Attribute::Text,
AttrValue::String(format!("DigitCounter has now value: {}", v))
)
.is_ok());
None
}
Msg::LetterCounterBlur => {
// Give focus to digit counter
assert!(self.app.active(&Id::DigitCounter).is_ok());
None
}
Msg::LetterCounterChanged(v) => {
// Update label
assert!(self
.app
.attr(
&Id::Label,
Attribute::Text,
AttrValue::String(format!("LetterCounter has now value: {}", v))
)
.is_ok());
None
}
}
} else {
None
}
}
}
+120
View File
@@ -0,0 +1,120 @@
//! ## Label
//!
//! label component
use std::ops::Add;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tuirealm::command::{Cmd, CmdResult};
use tuirealm::props::{Alignment, Color, TextModifiers};
use tuirealm::ratatui::layout::Rect;
use tuirealm::{
AttrValue, Attribute, Component, Event, Frame, MockComponent, NoUserEvent, State, StateValue,
};
use super::{Label, Msg};
/// Simple clock component which displays current time
pub struct Clock {
component: Label,
states: OwnStates,
}
impl Clock {
pub fn new(initial_time: SystemTime) -> Self {
Self {
component: Label::default(),
states: OwnStates::new(initial_time),
}
}
pub fn alignment(mut self, a: Alignment) -> Self {
self.component
.attr(Attribute::TextAlign, AttrValue::Alignment(a));
self
}
pub fn foreground(mut self, c: Color) -> Self {
self.component
.attr(Attribute::Foreground, AttrValue::Color(c));
self
}
pub fn background(mut self, c: Color) -> Self {
self.component
.attr(Attribute::Background, AttrValue::Color(c));
self
}
pub fn modifiers(mut self, m: TextModifiers) -> Self {
self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
self
}
fn time_to_str(&self) -> String {
let since_the_epoch = self.get_epoch_time();
let hours = (since_the_epoch / 3600) % 24;
let minutes = (since_the_epoch / 60) % 60;
let seconds = since_the_epoch % 60;
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
}
fn get_epoch_time(&self) -> u64 {
self.states
.time
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs()
}
}
impl MockComponent for Clock {
fn view(&mut self, frame: &mut Frame, area: Rect) {
// Render
self.component.view(frame, area);
}
fn query(&self, attr: Attribute) -> Option<AttrValue> {
self.component.query(attr)
}
fn attr(&mut self, attr: Attribute, value: AttrValue) {
self.component.attr(attr, value);
}
fn state(&self) -> State {
// Return current time
State::One(StateValue::U64(self.get_epoch_time()))
}
fn perform(&mut self, cmd: Cmd) -> CmdResult {
self.component.perform(cmd)
}
}
impl Component<Msg, NoUserEvent> for Clock {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
if let Event::Tick = ev {
self.states.tick();
// Set text
self.attr(Attribute::Text, AttrValue::String(self.time_to_str()));
Some(Msg::Clock)
} else {
None
}
}
}
struct OwnStates {
time: SystemTime,
}
impl OwnStates {
pub fn new(time: SystemTime) -> Self {
Self { time }
}
pub fn tick(&mut self) {
self.time = self.time.add(Duration::from_secs(1));
}
}
+273
View File
@@ -0,0 +1,273 @@
//! ## Label
//!
//! label component
use tuirealm::command::{Cmd, CmdResult};
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::props::{Alignment, Borders, Color, Style, TextModifiers};
use tuirealm::ratatui::layout::Rect;
use tuirealm::ratatui::widgets::{BorderType, Paragraph};
use tuirealm::{
AttrValue, Attribute, Component, Event, Frame, MockComponent, NoUserEvent, Props, State,
StateValue,
};
use super::{get_block, Msg};
/// Counter which increments its value on Submit
struct Counter {
props: Props,
states: OwnStates,
}
impl Default for Counter {
fn default() -> Self {
Self {
props: Props::default(),
states: OwnStates::default(),
}
}
}
impl Counter {
pub fn label<S>(mut self, label: S) -> Self
where
S: AsRef<str>,
{
self.attr(
Attribute::Title,
AttrValue::Title((label.as_ref().to_string(), Alignment::Center)),
);
self
}
pub fn value(mut self, n: isize) -> Self {
self.attr(Attribute::Value, AttrValue::Number(n));
self
}
pub fn alignment(mut self, a: Alignment) -> Self {
self.attr(Attribute::TextAlign, AttrValue::Alignment(a));
self
}
pub fn foreground(mut self, c: Color) -> Self {
self.attr(Attribute::Foreground, AttrValue::Color(c));
self
}
pub fn background(mut self, c: Color) -> Self {
self.attr(Attribute::Background, AttrValue::Color(c));
self
}
pub fn modifiers(mut self, m: TextModifiers) -> Self {
self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
self
}
pub fn borders(mut self, b: Borders) -> Self {
self.attr(Attribute::Borders, AttrValue::Borders(b));
self
}
}
impl MockComponent for Counter {
fn view(&mut self, frame: &mut Frame, area: Rect) {
// Check if visible
if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
// Get properties
let text = self.states.counter.to_string();
let alignment = self
.props
.get_or(Attribute::TextAlign, AttrValue::Alignment(Alignment::Left))
.unwrap_alignment();
let foreground = self
.props
.get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
.unwrap_color();
let background = self
.props
.get_or(Attribute::Background, AttrValue::Color(Color::Reset))
.unwrap_color();
let modifiers = self
.props
.get_or(
Attribute::TextProps,
AttrValue::TextModifiers(TextModifiers::empty()),
)
.unwrap_text_modifiers();
let title = self
.props
.get_or(
Attribute::Title,
AttrValue::Title((String::default(), Alignment::Center)),
)
.unwrap_title();
let borders = self
.props
.get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
.unwrap_borders();
let focus = self
.props
.get_or(Attribute::Focus, AttrValue::Flag(false))
.unwrap_flag();
frame.render_widget(
Paragraph::new(text)
.block(get_block(borders, title, focus))
.style(
Style::default()
.fg(foreground)
.bg(background)
.add_modifier(modifiers),
)
.alignment(alignment),
area,
);
}
}
fn query(&self, attr: Attribute) -> Option<AttrValue> {
self.props.get(attr)
}
fn attr(&mut self, attr: Attribute, value: AttrValue) {
self.props.set(attr, value);
}
fn state(&self) -> State {
State::One(StateValue::Isize(self.states.counter))
}
fn perform(&mut self, cmd: Cmd) -> CmdResult {
match cmd {
Cmd::Submit => {
self.states.incr();
CmdResult::Changed(self.state())
}
_ => CmdResult::None,
}
}
}
struct OwnStates {
counter: isize,
}
impl Default for OwnStates {
fn default() -> Self {
Self { counter: 0 }
}
}
impl OwnStates {
fn incr(&mut self) {
self.counter += 1;
}
}
// -- Counter components
#[derive(MockComponent)]
pub struct LetterCounter {
component: Counter,
}
impl LetterCounter {
pub fn new(initial_value: isize) -> Self {
Self {
component: Counter::default()
.alignment(Alignment::Center)
.background(Color::Reset)
.borders(
Borders::default()
.color(Color::LightGreen)
.modifiers(BorderType::Rounded),
)
.foreground(Color::LightGreen)
.modifiers(TextModifiers::BOLD)
.value(initial_value)
.label("Letter counter"),
}
}
}
impl Component<Msg, NoUserEvent> for LetterCounter {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
// Get command
let cmd = match ev {
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) if ch.is_alphabetic() => Cmd::Submit,
Event::Keyboard(KeyEvent {
code: Key::Tab,
modifiers: KeyModifiers::NONE,
}) => return Some(Msg::LetterCounterBlur), // Return focus lost
Event::Keyboard(KeyEvent {
code: Key::Esc,
modifiers: KeyModifiers::NONE,
}) => return Some(Msg::AppClose),
_ => Cmd::None,
};
// perform
match self.perform(cmd) {
CmdResult::Changed(State::One(StateValue::Isize(c))) => {
Some(Msg::LetterCounterChanged(c))
}
_ => None,
}
}
}
#[derive(MockComponent)]
pub struct DigitCounter {
component: Counter,
}
impl DigitCounter {
pub fn new(initial_value: isize) -> Self {
Self {
component: Counter::default()
.alignment(Alignment::Center)
.background(Color::Reset)
.borders(
Borders::default()
.color(Color::Yellow)
.modifiers(BorderType::Rounded),
)
.foreground(Color::Yellow)
.modifiers(TextModifiers::BOLD)
.value(initial_value)
.label("Digit counter"),
}
}
}
impl Component<Msg, NoUserEvent> for DigitCounter {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
// Get command
let cmd = match ev {
Event::Keyboard(KeyEvent {
code: Key::Char(ch),
modifiers: KeyModifiers::NONE,
}) if ch.is_digit(10) => Cmd::Submit,
Event::Keyboard(KeyEvent {
code: Key::Tab,
modifiers: KeyModifiers::NONE,
}) => return Some(Msg::DigitCounterBlur), // Return focus lost
Event::Keyboard(KeyEvent {
code: Key::Esc,
modifiers: KeyModifiers::NONE,
}) => return Some(Msg::AppClose),
_ => Cmd::None,
};
// perform
match self.perform(cmd) {
CmdResult::Changed(State::One(StateValue::Isize(c))) => {
Some(Msg::DigitCounterChanged(c))
}
_ => None,
}
}
}
+124
View File
@@ -0,0 +1,124 @@
//! ## Label
//!
//! label component
use tuirealm::command::{Cmd, CmdResult};
use tuirealm::props::{Alignment, Color, Style, TextModifiers};
use tuirealm::ratatui::layout::Rect;
use tuirealm::ratatui::widgets::Paragraph;
use tuirealm::{
AttrValue, Attribute, Component, Event, Frame, MockComponent, NoUserEvent, Props, State,
};
use super::Msg;
/// Simple label component; just renders a text
/// NOTE: since I need just one label, I'm not going to use different object; I will directly implement Component for Label.
/// This is not ideal actually and in a real app you should differentiate Mock Components from Application Components.
pub struct Label {
props: Props,
}
impl Default for Label {
fn default() -> Self {
Self {
props: Props::default(),
}
}
}
impl Label {
pub fn text<S>(mut self, s: S) -> Self
where
S: AsRef<str>,
{
self.attr(Attribute::Text, AttrValue::String(s.as_ref().to_string()));
self
}
pub fn alignment(mut self, a: Alignment) -> Self {
self.attr(Attribute::TextAlign, AttrValue::Alignment(a));
self
}
pub fn foreground(mut self, c: Color) -> Self {
self.attr(Attribute::Foreground, AttrValue::Color(c));
self
}
pub fn background(mut self, c: Color) -> Self {
self.attr(Attribute::Background, AttrValue::Color(c));
self
}
pub fn modifiers(mut self, m: TextModifiers) -> Self {
self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
self
}
}
impl MockComponent for Label {
fn view(&mut self, frame: &mut Frame, area: Rect) {
// Check if visible
if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
// Get properties
let text = self
.props
.get_or(Attribute::Text, AttrValue::String(String::default()))
.unwrap_string();
let alignment = self
.props
.get_or(Attribute::TextAlign, AttrValue::Alignment(Alignment::Left))
.unwrap_alignment();
let foreground = self
.props
.get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
.unwrap_color();
let background = self
.props
.get_or(Attribute::Background, AttrValue::Color(Color::Reset))
.unwrap_color();
let modifiers = self
.props
.get_or(
Attribute::TextProps,
AttrValue::TextModifiers(TextModifiers::empty()),
)
.unwrap_text_modifiers();
frame.render_widget(
Paragraph::new(text)
.style(
Style::default()
.fg(foreground)
.bg(background)
.add_modifier(modifiers),
)
.alignment(alignment),
area,
);
}
}
fn query(&self, attr: Attribute) -> Option<AttrValue> {
self.props.get(attr)
}
fn attr(&mut self, attr: Attribute, value: AttrValue) {
self.props.set(attr, value);
}
fn state(&self) -> State {
State::None
}
fn perform(&mut self, _: Cmd) -> CmdResult {
CmdResult::None
}
}
impl Component<Msg, NoUserEvent> for Label {
fn on(&mut self, _: Event<NoUserEvent>) -> Option<Msg> {
// Does nothing
None
}
}
+33
View File
@@ -0,0 +1,33 @@
//! ## Components
//!
//! demo example components
use tuirealm::props::{Alignment, Borders, Color, Style};
use tuirealm::ratatui::widgets::Block;
use super::Msg;
// -- modules
mod clock;
mod counter;
mod label;
// -- export
pub use clock::Clock;
pub use counter::{DigitCounter, LetterCounter};
pub use label::Label;
/// ### get_block
///
/// Get block
pub(crate) fn get_block<'a>(props: Borders, title: (String, Alignment), focus: bool) -> Block<'a> {
Block::default()
.borders(props.sides)
.border_style(match focus {
true => props.style(),
false => Style::default().fg(Color::Reset).bg(Color::Reset),
})
.border_type(props.modifiers)
.title(title.0)
.title_alignment(title.1)
}
+76 -2
View File
@@ -1,3 +1,77 @@
fn main() {
println!("Hello, world!");
//! ## Demo
//!
//! `Demo` shows how to use tui-realm in a real case
extern crate tuirealm;
use tuirealm::application::PollStrategy;
use tuirealm::{AttrValue, Attribute, Update};
// -- internal
mod app;
mod components;
use app::model::Model;
// Let's define the messages handled by our app. NOTE: it must derive `PartialEq`
#[derive(Debug, PartialEq)]
pub enum Msg {
AppClose,
Clock,
DigitCounterChanged(isize),
DigitCounterBlur,
LetterCounterChanged(isize),
LetterCounterBlur,
}
// Let's define the component ids for our application
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub enum Id {
Clock,
DigitCounter,
LetterCounter,
Label,
}
fn main() {
// Setup model
let mut model = Model::default();
// Enter alternate screen
let _ = model.terminal.enter_alternate_screen();
let _ = model.terminal.enable_raw_mode();
// Main loop
// NOTE: loop until quit; quit is set in update if AppClose is received from counter
while !model.quit {
// Tick
match model.app.tick(PollStrategy::Once) {
Err(err) => {
assert!(model
.app
.attr(
&Id::Label,
Attribute::Text,
AttrValue::String(format!("Application error: {}", err)),
)
.is_ok());
}
Ok(messages) if messages.len() > 0 => {
// NOTE: redraw if at least one msg has been processed
model.redraw = true;
for msg in messages.into_iter() {
let mut msg = Some(msg);
while msg.is_some() {
msg = model.update(msg);
}
}
}
_ => {}
}
// Redraw
if model.redraw {
model.view();
model.redraw = false;
}
}
// Terminate terminal
let _ = model.terminal.leave_alternate_screen();
let _ = model.terminal.disable_raw_mode();
let _ = model.terminal.clear_screen();
}