feat: add rustbox and crossterm demo

This commit is contained in:
Florian Dehau 2019-02-10 22:35:44 +01:00
parent cd41ca571f
commit f20512b599
11 changed files with 536 additions and 338 deletions

View File

@ -38,11 +38,16 @@ failure = "0.1"
structopt = "0.2"
[[example]]
name = "rustbox"
path = "examples/rustbox.rs"
name = "termion_demo"
path = "examples/termion_demo.rs"
required-features = ["termion"]
[[example]]
name = "rustbox_demo"
path = "examples/rustbox_demo.rs"
required-features = ["rustbox"]
[[example]]
name = "crossterm"
path = "examples/crossterm.rs"
name = "crossterm_demo"
path = "examples/crossterm_demo.rs"
required-features = ["crossterm"]

View File

@ -36,7 +36,16 @@ you may rely on the previously cited libraries to achieve such features.
### Demo
The [source code](examples/demo.rs) of the demo gif.
The demo shown in the gif can be run with all available backends
(`exmples/*_demo.rs` files). For example to see the `termion` version one could
run:
```
cargo run --example termion_demo --release --tick-rate 200
```
The UI code is in [examples/demo/ui.rs](examples/demo/ui.rs) while the
application state is in [examples/demo/app.rs](examples/demo/app.rs).
### Widgets
@ -56,6 +65,8 @@ The library comes with the following list of widgets:
Click on each item to see the source of the example. Run the examples with with
cargo (e.g. to run the demo `cargo run --example demo`), and quit by pressing `q`.
You can run all examples by running `make run-examples`.
### Third-party widgets
* [tui-logger](https://github.com/gin66/tui-logger)

View File

@ -1,38 +0,0 @@
use tui::backend::CrosstermBackend;
use tui::style::{Color, Modifier, Style};
use tui::widgets::{Block, Borders, Paragraph, Text, Widget};
use tui::Terminal;
fn main() -> Result<(), failure::Error> {
let mut terminal = Terminal::new(CrosstermBackend::new())?;
terminal.clear()?;
terminal.hide_cursor()?;
loop {
terminal.draw(|mut f| {
let size = f.size();
let text = [
Text::raw("It "),
Text::styled("works", Style::default().fg(Color::Yellow)),
];
Paragraph::new(text.iter())
.block(
Block::default()
.title("Crossterm Backend")
.title_style(Style::default().fg(Color::Yellow).modifier(Modifier::Bold))
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Magenta)),
)
.render(&mut f, size);
})?;
let input = crossterm::input();
match input.read_char()? {
'q' => {
break;
}
_ => {}
};
}
Ok(())
}

View File

@ -0,0 +1,90 @@
#[allow(dead_code)]
mod demo;
#[allow(dead_code)]
mod util;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use structopt::StructOpt;
use tui::backend::CrosstermBackend;
use tui::Terminal;
use crate::demo::{ui, App};
enum Event<I> {
Input(I),
Tick,
}
#[derive(Debug, StructOpt)]
struct Cli {
#[structopt(long = "tick-rate", default_value = "250")]
tick_rate: u64,
#[structopt(long = "log")]
log: bool,
}
fn main() -> Result<(), failure::Error> {
let cli = Cli::from_args();
stderrlog::new().quiet(!cli.log).verbosity(4).init()?;
let backend = CrosstermBackend::new();
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
// Setup input handling
let (tx, rx) = mpsc::channel();
{
let tx = tx.clone();
thread::spawn(move || {
let input = crossterm::input();
loop {
match input.read_char() {
Ok(key) => {
if let Err(_) = tx.send(Event::Input(key)) {
return;
}
if key == 'q' {
return;
}
}
Err(_) => {}
}
}
});
}
{
let tx = tx.clone();
thread::spawn(move || {
let tx = tx.clone();
loop {
tx.send(Event::Tick).unwrap();
thread::sleep(Duration::from_millis(cli.tick_rate));
}
});
}
let mut app = App::new("Crossterm Demo");
terminal.clear()?;
loop {
ui::draw(&mut terminal, &app)?;
match rx.recv()? {
Event::Input(key) => {
// TODO: handle key events once they are supported by crossterm
app.on_key(key);
}
Event::Tick => {
app.on_tick();
}
}
if app.should_quit {
break;
}
}
Ok(())
}

249
examples/demo/app.rs Normal file
View File

@ -0,0 +1,249 @@
use crate::util::{RandomSignal, SinSignal, TabsState};
const TASKS: [&'static str; 24] = [
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10",
"Item11", "Item12", "Item13", "Item14", "Item15", "Item16", "Item17", "Item18", "Item19",
"Item20", "Item21", "Item22", "Item23", "Item24",
];
const LOGS: [(&'static str, &'static str); 26] = [
("Event1", "INFO"),
("Event2", "INFO"),
("Event3", "CRITICAL"),
("Event4", "ERROR"),
("Event5", "INFO"),
("Event6", "INFO"),
("Event7", "WARNING"),
("Event8", "INFO"),
("Event9", "INFO"),
("Event10", "INFO"),
("Event11", "CRITICAL"),
("Event12", "INFO"),
("Event13", "INFO"),
("Event14", "INFO"),
("Event15", "INFO"),
("Event16", "INFO"),
("Event17", "ERROR"),
("Event18", "ERROR"),
("Event19", "INFO"),
("Event20", "INFO"),
("Event21", "WARNING"),
("Event22", "INFO"),
("Event23", "INFO"),
("Event24", "WARNING"),
("Event25", "INFO"),
("Event26", "INFO"),
];
const EVENTS: [(&'static str, u64); 24] = [
("B1", 9),
("B2", 12),
("B3", 5),
("B4", 8),
("B5", 2),
("B6", 4),
("B7", 5),
("B8", 9),
("B9", 14),
("B10", 15),
("B11", 1),
("B12", 0),
("B13", 4),
("B14", 6),
("B15", 4),
("B16", 6),
("B17", 4),
("B18", 7),
("B19", 13),
("B20", 8),
("B21", 11),
("B22", 9),
("B23", 3),
("B24", 5),
];
pub struct Signal<S: Iterator> {
source: S,
pub points: Vec<S::Item>,
tick_rate: usize,
}
impl<S> Signal<S>
where
S: Iterator,
{
fn on_tick(&mut self) {
for _ in 0..self.tick_rate {
self.points.remove(0);
}
self.points
.extend(self.source.by_ref().take(self.tick_rate));
}
}
pub struct Signals {
pub sin1: Signal<SinSignal>,
pub sin2: Signal<SinSignal>,
pub window: [f64; 2],
}
impl Signals {
fn on_tick(&mut self) {
self.sin1.on_tick();
self.sin2.on_tick();
self.window[0] += 1.0;
self.window[1] += 1.0;
}
}
pub struct ListState<I> {
pub items: Vec<I>,
pub selected: usize,
}
impl<I> ListState<I> {
fn new(items: Vec<I>) -> ListState<I> {
ListState { items, selected: 0 }
}
fn select_previous(&mut self) {
if self.selected > 0 {
self.selected -= 1;
}
}
fn select_next(&mut self) {
if self.selected < self.items.len() - 1 {
self.selected += 1
}
}
}
pub struct Server<'a> {
pub name: &'a str,
pub location: &'a str,
pub coords: (f64, f64),
pub status: &'a str,
}
pub struct App<'a> {
pub title: &'a str,
pub should_quit: bool,
pub tabs: TabsState<'a>,
pub show_chart: bool,
pub progress: u16,
pub sparkline: Signal<RandomSignal>,
pub tasks: ListState<(&'a str)>,
pub logs: ListState<(&'a str, &'a str)>,
pub signals: Signals,
pub barchart: Vec<(&'a str, u64)>,
pub servers: Vec<Server<'a>>,
}
impl<'a> App<'a> {
pub fn new(title: &'a str) -> App<'a> {
let mut rand_signal = RandomSignal::new(0, 100);
let sparkline_points = rand_signal.by_ref().take(300).collect();
let mut sin_signal = SinSignal::new(0.2, 3.0, 18.0);
let sin1_points = sin_signal.by_ref().take(100).collect();
let mut sin_signal2 = SinSignal::new(0.1, 2.0, 10.0);
let sin2_points = sin_signal2.by_ref().take(200).collect();
App {
title,
should_quit: false,
tabs: TabsState::new(vec!["Tab0", "Tab1"]),
show_chart: true,
progress: 0,
sparkline: Signal {
source: rand_signal,
points: sparkline_points,
tick_rate: 1,
},
tasks: ListState::new(TASKS.to_vec()),
logs: ListState::new(LOGS.to_vec()),
signals: Signals {
sin1: Signal {
source: sin_signal,
points: sin1_points,
tick_rate: 5,
},
sin2: Signal {
source: sin_signal2,
points: sin2_points,
tick_rate: 10,
},
window: [0.0, 20.0],
},
barchart: EVENTS.to_vec(),
servers: vec![
Server {
name: "NorthAmerica-1",
location: "New York City",
coords: (40.71, -74.00),
status: "Up",
},
Server {
name: "Europe-1",
location: "Paris",
coords: (48.85, 2.35),
status: "Failure",
},
Server {
name: "SouthAmerica-1",
location: "São Paulo",
coords: (-23.54, -46.62),
status: "Up",
},
Server {
name: "Asia-1",
location: "Singapore",
coords: (1.35, 103.86),
status: "Up",
},
],
}
}
pub fn on_up(&mut self) {
self.tasks.select_previous();
}
pub fn on_down(&mut self) {
self.tasks.select_next();
}
pub fn on_right(&mut self) {
self.tabs.next();
}
pub fn on_left(&mut self) {
self.tabs.previous();
}
pub fn on_key(&mut self, c: char) {
match c {
'q' => {
self.should_quit = true;
}
't' => {
self.show_chart = !self.show_chart;
}
_ => {}
}
}
pub fn on_tick(&mut self) {
// Update progress
self.progress += 5;
if self.progress > 100 {
self.progress = 0;
}
self.sparkline.on_tick();
self.signals.on_tick();
let log = self.logs.items.pop().unwrap();
self.logs.items.insert(0, log);
let event = self.barchart.pop().unwrap();
self.barchart.insert(0, event);
}
}

3
examples/demo/mod.rs Normal file
View File

@ -0,0 +1,3 @@
mod app;
pub mod ui;
pub use app::App;

View File

@ -1,15 +1,6 @@
#[allow(dead_code)]
mod util;
use std::io;
use std::time::Duration;
use structopt::StructOpt;
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::{Backend, TermionBackend};
use tui::backend::Backend;
use tui::layout::{Constraint, Direction, Layout, Rect};
use tui::style::{Color, Modifier, Style};
use tui::widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle};
@ -19,235 +10,26 @@ use tui::widgets::{
};
use tui::{Frame, Terminal};
use crate::util::event::{Config, Event, Events};
use crate::util::{RandomSignal, SinSignal, TabsState};
use crate::demo::App;
struct Server<'a> {
name: &'a str,
location: &'a str,
coords: (f64, f64),
status: &'a str,
}
struct App<'a> {
items: Vec<&'a str>,
events: Vec<(&'a str, &'a str)>,
selected: usize,
tabs: TabsState<'a>,
show_chart: bool,
progress: u16,
data: Vec<u64>,
data2: Vec<(f64, f64)>,
data3: Vec<(f64, f64)>,
data4: Vec<(&'a str, u64)>,
window: [f64; 2],
colors: [Color; 2],
color_index: usize,
servers: Vec<Server<'a>>,
}
#[derive(Debug, StructOpt)]
struct Cli {
#[structopt(long = "tick-rate", default_value = "250")]
tick_rate: u64,
#[structopt(long = "log")]
log: bool,
}
fn main() -> Result<(), failure::Error> {
let cli = Cli::from_args();
stderrlog::new().quiet(!cli.log).verbosity(4).init()?;
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let events = Events::with_config(Config {
tick_rate: Duration::from_millis(cli.tick_rate),
..Config::default()
});
let mut rand_signal = RandomSignal::new(0, 100);
let mut sin_signal = SinSignal::new(0.2, 3.0, 18.0);
let mut sin_signal2 = SinSignal::new(0.1, 2.0, 10.0);
let mut app = App {
items: vec![
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9",
"Item10", "Item11", "Item12", "Item13", "Item14", "Item15", "Item16", "Item17",
"Item18", "Item19", "Item20", "Item21", "Item22", "Item23", "Item24",
],
events: vec![
("Event1", "INFO"),
("Event2", "INFO"),
("Event3", "CRITICAL"),
("Event4", "ERROR"),
("Event5", "INFO"),
("Event6", "INFO"),
("Event7", "WARNING"),
("Event8", "INFO"),
("Event9", "INFO"),
("Event10", "INFO"),
("Event11", "CRITICAL"),
("Event12", "INFO"),
("Event13", "INFO"),
("Event14", "INFO"),
("Event15", "INFO"),
("Event16", "INFO"),
("Event17", "ERROR"),
("Event18", "ERROR"),
("Event19", "INFO"),
("Event20", "INFO"),
("Event21", "WARNING"),
("Event22", "INFO"),
("Event23", "INFO"),
("Event24", "WARNING"),
("Event25", "INFO"),
("Event26", "INFO"),
],
selected: 0,
tabs: TabsState::new(vec!["Tab0", "Tab1"]),
show_chart: true,
progress: 0,
data: rand_signal.by_ref().take(300).collect(),
data2: sin_signal.by_ref().take(100).collect(),
data3: sin_signal2.by_ref().take(200).collect(),
data4: vec![
("B1", 9),
("B2", 12),
("B3", 5),
("B4", 8),
("B5", 2),
("B6", 4),
("B7", 5),
("B8", 9),
("B9", 14),
("B10", 15),
("B11", 1),
("B12", 0),
("B13", 4),
("B14", 6),
("B15", 4),
("B16", 6),
("B17", 4),
("B18", 7),
("B19", 13),
("B20", 8),
("B21", 11),
("B22", 9),
("B23", 3),
("B24", 5),
],
window: [0.0, 20.0],
colors: [Color::Magenta, Color::Red],
color_index: 0,
servers: vec![
Server {
name: "NorthAmerica-1",
location: "New York City",
coords: (40.71, -74.00),
status: "Up",
},
Server {
name: "Europe-1",
location: "Paris",
coords: (48.85, 2.35),
status: "Failure",
},
Server {
name: "SouthAmerica-1",
location: "São Paulo",
coords: (-23.54, -46.62),
status: "Up",
},
Server {
name: "Asia-1",
location: "Singapore",
coords: (1.35, 103.86),
status: "Up",
},
],
};
loop {
// Draw UI
terminal.draw(|mut f| {
let chunks = Layout::default()
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
.split(f.size());
Tabs::default()
.block(Block::default().borders(Borders::ALL).title("Tabs"))
.titles(&app.tabs.titles)
.style(Style::default().fg(Color::Green))
.highlight_style(Style::default().fg(Color::Yellow))
.select(app.tabs.index)
.render(&mut f, chunks[0]);
match app.tabs.index {
0 => draw_first_tab(&mut f, &app, chunks[1]),
1 => draw_second_tab(&mut f, &app, chunks[1]),
_ => {}
};
})?;
match events.next()? {
Event::Input(input) => match input {
Key::Char('q') => {
break;
}
Key::Up => {
if app.selected > 0 {
app.selected -= 1
};
}
Key::Down => {
if app.selected < app.items.len() - 1 {
app.selected += 1;
}
}
Key::Left => {
app.tabs.previous();
}
Key::Right => {
app.tabs.next();
}
Key::Char('t') => {
app.show_chart = !app.show_chart;
}
_ => {}
},
Event::Tick => {
app.progress += 5;
if app.progress > 100 {
app.progress = 0;
}
app.data.insert(0, rand_signal.next().unwrap());
app.data.pop();
for _ in 0..5 {
app.data2.remove(0);
app.data2.push(sin_signal.next().unwrap());
}
for _ in 0..10 {
app.data3.remove(0);
app.data3.push(sin_signal2.next().unwrap());
}
let i = app.data4.pop().unwrap();
app.data4.insert(0, i);
app.window[0] += 1.0;
app.window[1] += 1.0;
let i = app.events.pop().unwrap();
app.events.insert(0, i);
app.color_index += 1;
if app.color_index >= app.colors.len() {
app.color_index = 0;
}
}
}
}
Ok(())
pub fn draw<B: Backend>(terminal: &mut Terminal<B>, app: &App) -> Result<(), io::Error> {
terminal.draw(|mut f| {
let chunks = Layout::default()
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
.split(f.size());
Tabs::default()
.block(Block::default().borders(Borders::ALL).title(app.title))
.titles(&app.tabs.titles)
.style(Style::default().fg(Color::Green))
.highlight_style(Style::default().fg(Color::Yellow))
.select(app.tabs.index)
.render(&mut f, chunks[0]);
match app.tabs.index {
0 => draw_first_tab(&mut f, &app, chunks[1]),
1 => draw_second_tab(&mut f, &app, chunks[1]),
_ => {}
};
})
}
fn draw_first_tab<B>(f: &mut Frame<B>, app: &App, area: Rect)
@ -295,7 +77,7 @@ where
Sparkline::default()
.block(Block::default().title("Sparkline:"))
.style(Style::default().fg(Color::Green))
.data(&app.data)
.data(&app.sparkline.points)
.render(f, chunks[1]);
}
@ -323,8 +105,8 @@ where
.split(chunks[0]);
SelectableList::default()
.block(Block::default().borders(Borders::ALL).title("List"))
.items(&app.items)
.select(Some(app.selected))
.items(&app.tasks.items)
.select(Some(app.tasks.selected))
.highlight_style(Style::default().fg(Color::Yellow).modifier(Modifier::Bold))
.highlight_symbol(">")
.render(f, chunks[0]);
@ -332,7 +114,7 @@ where
let warning_style = Style::default().fg(Color::Yellow);
let error_style = Style::default().fg(Color::Magenta);
let critical_style = Style::default().fg(Color::Red);
let events = app.events.iter().map(|&(evt, level)| {
let events = app.logs.items.iter().map(|&(evt, level)| {
Text::styled(
format!("{}: {}", level, evt),
match level {
@ -349,7 +131,7 @@ where
}
BarChart::default()
.block(Block::default().borders(Borders::ALL).title("Bar chart"))
.data(&app.data4)
.data(&app.barchart)
.bar_width(3)
.bar_gap(2)
.value_style(
@ -375,11 +157,11 @@ where
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::Italic))
.bounds(app.window)
.bounds(app.signals.window)
.labels(&[
&format!("{}", app.window[0]),
&format!("{}", (app.window[0] + app.window[1]) / 2.0),
&format!("{}", app.window[1]),
&format!("{}", app.signals.window[0]),
&format!("{}", (app.signals.window[0] + app.signals.window[1]) / 2.0),
&format!("{}", app.signals.window[1]),
]),
)
.y_axis(
@ -395,12 +177,12 @@ where
.name("data2")
.marker(Marker::Dot)
.style(Style::default().fg(Color::Cyan))
.data(&app.data2),
.data(&app.signals.sin1.points),
Dataset::default()
.name("data3")
.marker(Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.data(&app.data3),
.data(&app.signals.sin2.points),
])
.render(f, chunks[1]);
}

View File

@ -1,46 +0,0 @@
use rustbox::Key;
use std::error::Error;
use tui::backend::RustboxBackend;
use tui::style::{Color, Modifier, Style};
use tui::widgets::{Block, Borders, Paragraph, Text, Widget};
use tui::Terminal;
fn main() -> Result<(), failure::Error> {
let mut terminal = Terminal::new(RustboxBackend::new().unwrap()).unwrap();
terminal.clear().unwrap();
terminal.hide_cursor().unwrap();
loop {
draw(&mut terminal)?;
match terminal.backend().rustbox().poll_event(false) {
Ok(rustbox::Event::KeyEvent(key)) => {
if key == Key::Char('q') {
break;
}
}
Err(e) => panic!("{}", e.description()),
_ => {}
};
}
terminal.show_cursor()?;
Ok(())
}
fn draw(t: &mut Terminal<RustboxBackend>) -> Result<(), std::io::Error> {
let text = [
Text::raw("It "),
Text::styled("works", Style::default().fg(Color::Yellow)),
];
t.draw(|mut f| {
let size = f.size();
Paragraph::new(text.iter())
.block(
Block::default()
.title("Rustbox backend")
.title_style(Style::default().fg(Color::Yellow).modifier(Modifier::Bold))
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Magenta)),
)
.render(&mut f, size)
})
}

66
examples/rustbox_demo.rs Normal file
View File

@ -0,0 +1,66 @@
mod demo;
#[allow(dead_code)]
mod util;
use std::time::{Duration, Instant};
use rustbox::keyboard::Key;
use structopt::StructOpt;
use tui::backend::RustboxBackend;
use tui::Terminal;
use crate::demo::{ui, App};
#[derive(Debug, StructOpt)]
struct Cli {
#[structopt(long = "tick-rate", default_value = "250")]
tick_rate: u64,
#[structopt(long = "log")]
log: bool,
}
fn main() -> Result<(), failure::Error> {
let cli = Cli::from_args();
stderrlog::new().quiet(!cli.log).verbosity(4).init()?;
let backend = RustboxBackend::new()?;
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let mut app = App::new("Rustbox demo");
let mut last_tick = Instant::now();
let tick_rate = Duration::from_millis(cli.tick_rate);
loop {
ui::draw(&mut terminal, &app)?;
match terminal.backend().rustbox().peek_event(tick_rate, false) {
Ok(rustbox::Event::KeyEvent(key)) => match key {
Key::Char(c) => {
app.on_key(c);
}
Key::Up => {
app.on_up();
}
Key::Down => {
app.on_down();
}
Key::Left => {
app.on_left();
}
Key::Right => {
app.on_right();
}
_ => {}
},
_ => {}
}
if last_tick.elapsed() > tick_rate {
app.on_tick();
last_tick = Instant::now();
}
if app.should_quit {
break;
}
}
Ok(())
}

75
examples/termion_demo.rs Normal file
View File

@ -0,0 +1,75 @@
mod demo;
#[allow(dead_code)]
mod util;
use std::io;
use std::time::Duration;
use structopt::StructOpt;
use termion::event::Key;
use termion::input::MouseTerminal;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::TermionBackend;
use tui::Terminal;
use crate::demo::{ui, App};
use crate::util::event::{Config, Event, Events};
#[derive(Debug, StructOpt)]
struct Cli {
#[structopt(long = "tick-rate", default_value = "250")]
tick_rate: u64,
#[structopt(long = "log")]
log: bool,
}
fn main() -> Result<(), failure::Error> {
let cli = Cli::from_args();
stderrlog::new().quiet(!cli.log).verbosity(4).init()?;
let events = Events::with_config(Config {
tick_rate: Duration::from_millis(cli.tick_rate),
..Config::default()
});
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let mut app = App::new("Termion demo");
loop {
ui::draw(&mut terminal, &app)?;
match events.next()? {
Event::Input(key) => match key {
Key::Char(c) => {
app.on_key(c);
}
Key::Up => {
app.on_up();
}
Key::Down => {
app.on_down();
}
Key::Left => {
app.on_left();
}
Key::Right => {
app.on_right();
}
_ => {}
},
Event::Tick => {
app.on_tick();
}
}
if app.should_quit {
break;
}
}
Ok(())
}

View File

@ -1,3 +1,4 @@
#[cfg(feature = "termion")]
pub mod event;
use rand::distributions::{Distribution, Uniform};