Compare commits
84 Commits
v0.3.0-bet
...
v0.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0ab1e906e | ||
|
|
f8b3526426 | ||
|
|
d83baab433 | ||
|
|
43e38ac483 | ||
|
|
b079d4da4c | ||
|
|
21e79ca078 | ||
|
|
a25bbea555 | ||
|
|
b7664a4108 | ||
|
|
d360cd3434 | ||
|
|
e037db076c | ||
|
|
3ef19f41e6 | ||
|
|
da90ec15fa | ||
|
|
7f5af46300 | ||
|
|
624e6ee047 | ||
|
|
4a1f3cd61f | ||
|
|
7c4a3d2b02 | ||
|
|
8db1bb56f2 | ||
|
|
d75198a8ee | ||
|
|
cadb41c9e3 | ||
|
|
b30cae0473 | ||
|
|
7290086fe9 | ||
|
|
bca920bea0 | ||
|
|
32de7a3fdc | ||
|
|
f20512b599 | ||
|
|
cd41ca571f | ||
|
|
dc654e9f6c | ||
|
|
f5d7f70472 | ||
|
|
0168442c22 | ||
|
|
22579b77cc | ||
|
|
09c09d2fd1 | ||
|
|
b669cf9ce7 | ||
|
|
5bc617a9a6 | ||
|
|
a75b811061 | ||
|
|
ec6b46324e | ||
|
|
97f764b45d | ||
|
|
7f31a55506 | ||
|
|
2286d097dc | ||
|
|
52a40ec99a | ||
|
|
a78fa73b34 | ||
|
|
d7e4a252fb | ||
|
|
1c0b0abf61 | ||
|
|
f7c6620e25 | ||
|
|
16372f7847 | ||
|
|
72c2eb7182 | ||
|
|
144bfb71cf | ||
|
|
3fd9e23851 | ||
|
|
10642d0e04 | ||
|
|
063ab2f87d | ||
|
|
1802cf8dbc | ||
|
|
090975481b | ||
|
|
228816f5f8 | ||
|
|
8522e028f1 | ||
|
|
a2776dfc86 | ||
|
|
cc95c8cfb0 | ||
|
|
89dac9d2a6 | ||
|
|
8cdfc883b9 | ||
|
|
b3689eceb7 | ||
|
|
5cee2afc6d | ||
|
|
50fef0fb26 | ||
|
|
4c46ef69e9 | ||
|
|
22e8fade7e | ||
|
|
37aa06f508 | ||
|
|
f6d2f8f929 | ||
|
|
32947669d5 | ||
|
|
fdf3015ad0 | ||
|
|
03bfcde147 | ||
|
|
56fc43400a | ||
|
|
7b4d35d224 | ||
|
|
a99fc115f8 | ||
|
|
d8e5f57d53 | ||
|
|
aa85e597d9 | ||
|
|
08ab92da80 | ||
|
|
5d52fd2486 | ||
|
|
4ae9850e13 | ||
|
|
e14190ae4b | ||
|
|
ce445a8096 | ||
|
|
dd71d6471c | ||
|
|
f795173886 | ||
|
|
e42ab1fed8 | ||
|
|
0544c023f5 | ||
|
|
ff47f9480b | ||
|
|
70561b7c54 | ||
|
|
559c9c75f3 | ||
|
|
6c69160d6b |
104
CHANGELOG.md
104
CHANGELOG.md
@@ -2,6 +2,110 @@
|
||||
|
||||
## To be released
|
||||
|
||||
## v0.5.0 - 2019-03-10
|
||||
|
||||
### Added
|
||||
|
||||
* Add a new curses backend (with Windows support thanks to `pancurses`).
|
||||
* Add `Backend::get_cursor` and `Backend::set_cursor` methods to query and
|
||||
set the position of the cursor.
|
||||
* Add more constructors to the `Crossterm` backend.
|
||||
* Add a demo for all backends using a shared UI and application state.
|
||||
* Add `Ratio` as a new variant of layout `Constraint`. It can be used to define
|
||||
exact ratios constraints.
|
||||
|
||||
### Changed
|
||||
|
||||
* Add support for multiple modifiers on the same `Style` by changing `Modifier`
|
||||
from an enum to a bitflags struct.
|
||||
|
||||
So instead of writing:
|
||||
|
||||
```rust
|
||||
let style = Style::default().modifier(Modifier::Italic);
|
||||
```
|
||||
|
||||
one should use:
|
||||
|
||||
```rust
|
||||
let style = Style::default().modifier(Modifier::ITALIC);
|
||||
// or
|
||||
let style = Style::default().modifier(Modifier::ITALIC | Modifier::BOLD);
|
||||
```
|
||||
|
||||
### Fixed
|
||||
|
||||
* Ensure correct behavoir of the alternate screens with the `Crossterm` backend.
|
||||
* Fix out of bounds panic when two `Buffer` are merged.
|
||||
|
||||
## v0.4.0 - 2019-02-03
|
||||
|
||||
### Added
|
||||
|
||||
* Add a new canvas shape: `Rectangle`.
|
||||
* Official support of `Crossterm` backend.
|
||||
* Make it possible to choose the divider between `Tabs`.
|
||||
* Add word wrapping on Paragraph.
|
||||
* The gauge widget accepts a ratio (f64 between 0 and 1) in addition of a
|
||||
percentage.
|
||||
|
||||
### Changed
|
||||
|
||||
* Upgrade to Rust 2018 edition.
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix rendering of double-width characters.
|
||||
* Fix race condition on the size of the terminal and expose a size that is
|
||||
safe to use when drawing through `Frame::size`.
|
||||
* Prevent unsigned int overflow on large screens.
|
||||
|
||||
## v0.3.0 - 2018-11-04
|
||||
|
||||
### Added
|
||||
|
||||
* Add experimental test backend
|
||||
|
||||
## v0.3.0-beta.3 - 2018-09-24
|
||||
|
||||
### Changed
|
||||
|
||||
* `show_cursor` is called when `Terminal` is dropped if the cursor is hidden.
|
||||
|
||||
## v0.3.0-beta.2 - 2018-09-23
|
||||
|
||||
### Changed
|
||||
|
||||
* Remove custom `termion` backends. This is motivated by the fact that
|
||||
`termion` structs are meant to be combined/wrapped to provide additional
|
||||
functionalities to the terminal (e.g AlternateScreen, Mouse support, ...).
|
||||
Thus providing exclusive types do not make a lot of sense and give a false
|
||||
hint that additional features cannot be used together. The recommended
|
||||
approach is now to create your own version of `stdout`:
|
||||
|
||||
```rust
|
||||
let stdout = io::stdout().into_raw_mode()?;
|
||||
let stdout = MouseTerminal::from(stdout);
|
||||
let stdout = AlternateScreen::from(stdout);
|
||||
```
|
||||
|
||||
and then to create the corresponding `termion` backend:
|
||||
|
||||
```rust
|
||||
let backend = TermionBackend::new(stdout);
|
||||
```
|
||||
|
||||
The resulting code is more verbose but it works with all combinations of
|
||||
additional `termion` features.
|
||||
|
||||
## v0.3.0-beta.1 - 2018-09-08
|
||||
|
||||
### Changed
|
||||
|
||||
* Replace `Item` by a generic and flexible `Text` that can be used in both
|
||||
`Paragraph` and `List` widgets.
|
||||
* Remove unecessary borrows on `Style`.
|
||||
|
||||
## v0.3.0-beta.0 - 2018-09-04
|
||||
|
||||
### Added
|
||||
|
||||
35
Cargo.toml
35
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tui"
|
||||
version = "0.3.0-beta.0"
|
||||
version = "0.5.0"
|
||||
authors = ["Florian Dehau <work@fdehau.com>"]
|
||||
description = """
|
||||
A library to build rich terminal user interfaces or dashboards
|
||||
@@ -8,37 +8,54 @@ A library to build rich terminal user interfaces or dashboards
|
||||
keywords = ["tui", "terminal", "dashboard"]
|
||||
repository = "https://github.com/fdehau/tui-rs"
|
||||
license = "MIT"
|
||||
exclude = ["docs/*", ".travis.yml"]
|
||||
exclude = ["assets/*", ".travis.yml"]
|
||||
autoexamples = true
|
||||
edition = "2018"
|
||||
|
||||
[badges]
|
||||
travis-ci = { repository = "fdehau/tui-rs" }
|
||||
appveyor = { repository = "fdehau/tui-rs" }
|
||||
|
||||
[features]
|
||||
default = ["termion"]
|
||||
curses = ["easycurses", "pancurses"]
|
||||
|
||||
[dependencies]
|
||||
bitflags = "1.0"
|
||||
cassowary = "0.3"
|
||||
itertools = "0.7"
|
||||
itertools = "0.8"
|
||||
log = "0.4"
|
||||
either = "1.5"
|
||||
unicode-segmentation = "1.2"
|
||||
unicode-width = "0.1"
|
||||
termion = { version = "1.5", optional = true }
|
||||
rustbox = { version = "0.11", optional = true }
|
||||
crossterm = { version = "0.4", optional = true }
|
||||
crossterm = { version = "0.6", optional = true }
|
||||
easycurses = { version = "0.12.2", optional = true }
|
||||
pancurses = { version = "0.16.1", optional = true, features = ["win32a"] }
|
||||
|
||||
[dev-dependencies]
|
||||
stderrlog = "0.4"
|
||||
rand = "0.4"
|
||||
rand = "0.6"
|
||||
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"]
|
||||
|
||||
[[example]]
|
||||
name = "curses_demo"
|
||||
path = "examples/curses_demo.rs"
|
||||
required-features = ["curses"]
|
||||
|
||||
40
Makefile
40
Makefile
@@ -1,5 +1,4 @@
|
||||
# Makefile for the tui-rs project (https://github.com/fdehau/tui-rs)
|
||||
|
||||
SHELL=/bin/bash
|
||||
|
||||
# ================================ Cargo ======================================
|
||||
|
||||
@@ -21,7 +20,7 @@ endif
|
||||
|
||||
# ================================ Help =======================================
|
||||
|
||||
|
||||
.PHONY: help
|
||||
help: ## Print all the available commands
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
||||
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||
@@ -29,39 +28,56 @@ help: ## Print all the available commands
|
||||
|
||||
# =============================== Build =======================================
|
||||
|
||||
.PHONY: check
|
||||
check: ## Validate the project code
|
||||
$(CARGO) check
|
||||
|
||||
.PHONY: build
|
||||
build: ## Build the project in debug mode
|
||||
$(CARGO) build $(CARGO_FLAGS)
|
||||
|
||||
.PHONY: release
|
||||
release: CARGO_FLAGS += --release
|
||||
release: build ## Build the project in release mode
|
||||
|
||||
|
||||
# ================================ Lint =======================================
|
||||
|
||||
RUSTFMT_WRITEMODE ?= 'diff'
|
||||
|
||||
.PHONY: lint
|
||||
lint: fmt clippy ## Lint project files
|
||||
|
||||
.PHONY: fmt
|
||||
fmt: ## Check the format of the source code
|
||||
cargo fmt --all -- --check
|
||||
|
||||
clippy: RUST_CHANNEL = nightly
|
||||
.PHONY: clippy
|
||||
clippy: ## Check the style of the source code and catch common errors
|
||||
$(CARGO) clippy --features="termion rustbox"
|
||||
$(CARGO) clippy --all-features
|
||||
|
||||
|
||||
# ================================ Test =======================================
|
||||
|
||||
|
||||
.PHONY: test
|
||||
test: ## Run the tests
|
||||
$(CARGO) test
|
||||
$(CARGO) test --all-features
|
||||
|
||||
# =============================== Examples ====================================
|
||||
|
||||
.PHONY: build-examples
|
||||
build-examples: ## Build all examples
|
||||
@$(CARGO) build --examples --all-features
|
||||
|
||||
.PHONY: run-examples
|
||||
run-examples: ## Run all examples
|
||||
@for file in examples/*.rs; do \
|
||||
name=$$(basename $${file/.rs/}); \
|
||||
$(CARGO) run --all-features --release --example $$name; \
|
||||
done;
|
||||
|
||||
# ================================ Doc ========================================
|
||||
|
||||
|
||||
.PHONY: doc
|
||||
doc: ## Build the documentation (available at ./target/doc)
|
||||
$(CARGO) doc
|
||||
|
||||
@@ -70,22 +86,28 @@ doc: ## Build the documentation (available at ./target/doc)
|
||||
|
||||
# Requires watchman and watchman-make (https://facebook.github.io/watchman/docs/install.html)
|
||||
|
||||
.PHONY: watch
|
||||
watch: ## Watch file changes and build the project if any
|
||||
watchman-make -p 'src/**/*.rs' -t check build
|
||||
|
||||
.PHONY: watch-test
|
||||
watch-test: ## Watch files changes and run the tests if any
|
||||
watchman-make -p 'src/**/*.rs' 'tests/**/*.rs' 'examples/**/*.rs' -t test
|
||||
|
||||
.PHONY: watch-doc
|
||||
watch-doc: ## Watch file changes and rebuild the documentation if any
|
||||
watchman-make -p 'src/**/*.rs' -t doc
|
||||
|
||||
# ================================= Pipelines =================================
|
||||
|
||||
.PHONY: stable
|
||||
stable: RUST_CHANNEL = stable
|
||||
stable: build test ## Run build and tests for stable
|
||||
|
||||
.PHONY: beta
|
||||
beta: RUST_CHANNEL = beta
|
||||
beta: build test ## Run build and tests for beta
|
||||
|
||||
.PHONY: nightly
|
||||
nightly: RUST_CHANNEL = nightly
|
||||
nightly: build lint test ## Run build, lint and tests for nightly
|
||||
|
||||
22
README.md
22
README.md
@@ -1,23 +1,26 @@
|
||||
# tui-rs
|
||||
|
||||
[](https://travis-ci.org/fdehau/tui-rs)
|
||||
[](https://ci.appveyor.com/project/fdehau/tui-rs/branch/master)
|
||||
[](https://crates.io/crates/tui)
|
||||
[](https://docs.rs/crate/tui/)
|
||||
|
||||
<img src="./docs/demo.gif" alt="Demo cast under Linux Termite with Inconsolata font 12pt">
|
||||
<img src="./assets/demo.gif" alt="Demo cast under Linux Termite with Inconsolata font 12pt">
|
||||
|
||||
`tui-rs` is a [Rust](https://www.rust-lang.org) library to build rich terminal
|
||||
user interfaces and dashboards. It is heavily inspired by the `Javascript`
|
||||
library [blessed-contrib](https://github.com/yaronn/blessed-contrib) and the
|
||||
`Go` library [termui](https://github.com/gizak/termui).
|
||||
|
||||
The library itself supports two different backends to draw to the terminal. You
|
||||
The library itself supports four different backends to draw to the terminal. You
|
||||
can either choose from:
|
||||
|
||||
- [termion](https://github.com/ticki/termion)
|
||||
- [rustbox](https://github.com/gchp/rustbox)
|
||||
- [crossterm](https://github.com/TimonPost/crossterm)
|
||||
- [pancurses](https://github.com/ihalila/pancurses)
|
||||
|
||||
However, some features may only be available in one of the two.
|
||||
However, some features may only be available in one of the three.
|
||||
|
||||
The library is based on the principle of immediate rendering with intermediate
|
||||
buffers. This means that at each new frame you should build all widgets that are
|
||||
@@ -34,7 +37,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
|
||||
|
||||
@@ -54,6 +66,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)
|
||||
|
||||
19
appveyor.yml
Normal file
19
appveyor.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
environment:
|
||||
matrix:
|
||||
# Stable channel
|
||||
- TARGET: x86_64-pc-windows-gnu
|
||||
CHANNEL: stable
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
CHANNEL: stable
|
||||
|
||||
install:
|
||||
- curl -sSf -o rustup-init.exe https://win.rustup.rs
|
||||
- rustup-init.exe --default-host %TARGET% --default-toolchain %CHANNEL% -y
|
||||
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
|
||||
- rustc -Vv
|
||||
- cargo -V
|
||||
|
||||
build: false
|
||||
|
||||
test_script:
|
||||
- cargo build --no-default-features --features crossterm
|
||||
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
@@ -1,29 +1,27 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use termion::event::Key;
|
||||
use termion::input::MouseTerminal;
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::screen::AlternateScreen;
|
||||
use tui::backend::TermionBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{BarChart, Block, Borders, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
|
||||
struct App<'a> {
|
||||
size: Rect,
|
||||
data: Vec<(&'a str, u64)>,
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
size: Rect::default(),
|
||||
data: vec![
|
||||
("B1", 9),
|
||||
("B2", 12),
|
||||
@@ -53,112 +51,77 @@ impl<'a> App<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
fn update(&mut self) {
|
||||
let value = self.data.pop().unwrap();
|
||||
self.data.insert(0, value);
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
Tick,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn main() -> Result<(), failure::Error> {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
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()?;
|
||||
|
||||
// Channels
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
let clock_tx = tx.clone();
|
||||
|
||||
// Input
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Tick
|
||||
thread::spawn(move || loop {
|
||||
clock_tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
});
|
||||
// Setup event handlers
|
||||
let events = Events::new();
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if app.size != size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
terminal.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
BarChart::default()
|
||||
.block(Block::default().title("Data1").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.bar_width(9)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow))
|
||||
.render(&mut f, chunks[0]);
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[1]);
|
||||
BarChart::default()
|
||||
.block(Block::default().title("Data2").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.bar_width(5)
|
||||
.bar_gap(3)
|
||||
.style(Style::default().fg(Color::Green))
|
||||
.value_style(Style::default().bg(Color::Green).modifier(Modifier::BOLD))
|
||||
.render(&mut f, chunks[0]);
|
||||
BarChart::default()
|
||||
.block(Block::default().title("Data3").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.style(Style::default().fg(Color::Red))
|
||||
.bar_width(7)
|
||||
.bar_gap(0)
|
||||
.value_style(Style::default().bg(Color::Red))
|
||||
.label_style(Style::default().fg(Color::Cyan).modifier(Modifier::ITALIC))
|
||||
.render(&mut f, chunks[1]);
|
||||
}
|
||||
})?;
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => if input == event::Key::Char('q') {
|
||||
break;
|
||||
},
|
||||
match events.next()? {
|
||||
Event::Input(input) => {
|
||||
if input == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Tick => {
|
||||
app.advance();
|
||||
app.update();
|
||||
}
|
||||
}
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(app.size);
|
||||
BarChart::default()
|
||||
.block(Block::default().title("Data1").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.bar_width(9)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow))
|
||||
.render(&mut f, chunks[0]);
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[1]);
|
||||
BarChart::default()
|
||||
.block(Block::default().title("Data2").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.bar_width(5)
|
||||
.bar_gap(3)
|
||||
.style(Style::default().fg(Color::Green))
|
||||
.value_style(Style::default().bg(Color::Green).modifier(Modifier::Bold))
|
||||
.render(&mut f, chunks[0]);
|
||||
BarChart::default()
|
||||
.block(Block::default().title("Data3").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.style(Style::default().fg(Color::Red))
|
||||
.bar_width(7)
|
||||
.bar_gap(0)
|
||||
.value_style(Style::default().bg(Color::Red))
|
||||
.label_style(Style::default().fg(Color::Cyan).modifier(Modifier::Italic))
|
||||
.render(&mut f, chunks[1]);
|
||||
}
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,84 +1,88 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use std::io;
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use termion::event::Key;
|
||||
use termion::input::MouseTerminal;
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::screen::AlternateScreen;
|
||||
use tui::backend::TermionBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Block, Borders, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
fn main() {
|
||||
let mut terminal = Terminal::new(MouseBackend::new().unwrap()).unwrap();
|
||||
let stdin = io::stdin();
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
use crate::util::event::{Event, Events};
|
||||
|
||||
let mut term_size = terminal.size().unwrap();
|
||||
draw(&mut terminal, term_size).unwrap();
|
||||
for c in stdin.keys() {
|
||||
let size = terminal.size().unwrap();
|
||||
if term_size != size {
|
||||
terminal.resize(size).unwrap();
|
||||
term_size = size;
|
||||
}
|
||||
draw(&mut terminal, term_size).unwrap();
|
||||
let evt = c.unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
fn main() -> Result<(), failure::Error> {
|
||||
// Terminal initialization
|
||||
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()?;
|
||||
|
||||
// Setup event handlers
|
||||
let events = Events::new();
|
||||
|
||||
loop {
|
||||
terminal.draw(|mut f| {
|
||||
// Wrapping block for a group
|
||||
// Just draw the block and the group on the same area and build the group
|
||||
// with at least a margin of 1
|
||||
let size = f.size();
|
||||
Block::default().borders(Borders::ALL).render(&mut f, size);
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(4)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[0]);
|
||||
Block::default()
|
||||
.title("With background")
|
||||
.title_style(Style::default().fg(Color::Yellow))
|
||||
.style(Style::default().bg(Color::Green))
|
||||
.render(&mut f, chunks[0]);
|
||||
Block::default()
|
||||
.title("Styled title")
|
||||
.title_style(
|
||||
Style::default()
|
||||
.fg(Color::White)
|
||||
.bg(Color::Red)
|
||||
.modifier(Modifier::BOLD),
|
||||
)
|
||||
.render(&mut f, chunks[1]);
|
||||
}
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[1]);
|
||||
Block::default()
|
||||
.title("With borders")
|
||||
.borders(Borders::ALL)
|
||||
.render(&mut f, chunks[0]);
|
||||
Block::default()
|
||||
.title("With styled borders")
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.render(&mut f, chunks[1]);
|
||||
}
|
||||
})?;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(key) => {
|
||||
if key == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
terminal.show_cursor().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, size: Rect) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
// Wrapping block for a group
|
||||
// Just draw the block and the group on the same area and build the group
|
||||
// with at least a margin of 1
|
||||
Block::default().borders(Borders::ALL).render(&mut f, size);
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(4)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(size);
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[0]);
|
||||
Block::default()
|
||||
.title("With background")
|
||||
.title_style(Style::default().fg(Color::Yellow))
|
||||
.style(Style::default().bg(Color::Green))
|
||||
.render(&mut f, chunks[0]);
|
||||
Block::default()
|
||||
.title("Styled title")
|
||||
.title_style(
|
||||
Style::default()
|
||||
.fg(Color::White)
|
||||
.bg(Color::Red)
|
||||
.modifier(Modifier::Bold),
|
||||
)
|
||||
.render(&mut f, chunks[1]);
|
||||
}
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[1]);
|
||||
Block::default()
|
||||
.title("With borders")
|
||||
.borders(Borders::ALL)
|
||||
.render(&mut f, chunks[0]);
|
||||
Block::default()
|
||||
.title("With styled borders")
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.render(&mut f, chunks[1]);
|
||||
}
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
use std::time::Duration;
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use termion::event::Key;
|
||||
use termion::input::MouseTerminal;
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::screen::AlternateScreen;
|
||||
use tui::backend::TermionBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use tui::style::Color;
|
||||
use tui::widgets::canvas::{Canvas, Line, Map, MapResolution};
|
||||
use tui::widgets::canvas::{Canvas, Map, MapResolution, Rectangle};
|
||||
use tui::widgets::{Block, Borders, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
use crate::util::event::{Config, Event, Events};
|
||||
|
||||
struct App {
|
||||
size: Rect,
|
||||
x: f64,
|
||||
y: f64,
|
||||
ball: Rect,
|
||||
@@ -31,7 +31,6 @@ struct App {
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App {
|
||||
size: Default::default(),
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
ball: Rect::new(10, 30, 10, 10),
|
||||
@@ -43,7 +42,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
fn update(&mut self) {
|
||||
if self.ball.left() < self.playground.left() || self.ball.right() > self.playground.right()
|
||||
{
|
||||
self.dir_x = !self.dir_x;
|
||||
@@ -67,138 +66,81 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
Tick,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn main() -> Result<(), failure::Error> {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
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()?;
|
||||
|
||||
// Channels
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
let clock_tx = tx.clone();
|
||||
|
||||
// Input
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Tick
|
||||
thread::spawn(move || loop {
|
||||
clock_tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
});
|
||||
// Setup event handlers
|
||||
let config = Config {
|
||||
tick_rate: Duration::from_millis(100),
|
||||
..Default::default()
|
||||
};
|
||||
let events = Events::with_config(config);
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
terminal.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("World"))
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
color: Color::White,
|
||||
resolution: MapResolution::High,
|
||||
});
|
||||
ctx.print(app.x, -app.y, "You are here", Color::Yellow);
|
||||
})
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0])
|
||||
.render(&mut f, chunks[0]);
|
||||
Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Pong"))
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Rectangle {
|
||||
rect: app.ball,
|
||||
color: Color::Yellow,
|
||||
});
|
||||
})
|
||||
.x_bounds([10.0, 110.0])
|
||||
.y_bounds([10.0, 110.0])
|
||||
.render(&mut f, chunks[1]);
|
||||
})?;
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
match events.next()? {
|
||||
Event::Input(input) => match input {
|
||||
event::Key::Char('q') => {
|
||||
Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
event::Key::Down => {
|
||||
Key::Down => {
|
||||
app.y += 1.0;
|
||||
}
|
||||
event::Key::Up => {
|
||||
Key::Up => {
|
||||
app.y -= 1.0;
|
||||
}
|
||||
event::Key::Right => {
|
||||
Key::Right => {
|
||||
app.x += 1.0;
|
||||
}
|
||||
event::Key::Left => {
|
||||
Key::Left => {
|
||||
app.x -= 1.0;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
},
|
||||
Event::Tick => {
|
||||
app.advance();
|
||||
app.update();
|
||||
}
|
||||
}
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(app.size);
|
||||
Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("World"))
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
color: Color::White,
|
||||
resolution: MapResolution::High,
|
||||
});
|
||||
ctx.print(app.x, -app.y, "You are here", Color::Yellow);
|
||||
})
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0])
|
||||
.render(&mut f, chunks[0]);
|
||||
Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Line {
|
||||
x1: f64::from(app.ball.left()),
|
||||
y1: f64::from(app.ball.top()),
|
||||
x2: f64::from(app.ball.right()),
|
||||
y2: f64::from(app.ball.top()),
|
||||
color: Color::Yellow,
|
||||
});
|
||||
ctx.draw(&Line {
|
||||
x1: f64::from(app.ball.right()),
|
||||
y1: f64::from(app.ball.top()),
|
||||
x2: f64::from(app.ball.right()),
|
||||
y2: f64::from(app.ball.bottom()),
|
||||
color: Color::Yellow,
|
||||
});
|
||||
ctx.draw(&Line {
|
||||
x1: f64::from(app.ball.right()),
|
||||
y1: f64::from(app.ball.bottom()),
|
||||
x2: f64::from(app.ball.left()),
|
||||
y2: f64::from(app.ball.bottom()),
|
||||
color: Color::Yellow,
|
||||
});
|
||||
ctx.draw(&Line {
|
||||
x1: f64::from(app.ball.left()),
|
||||
y1: f64::from(app.ball.bottom()),
|
||||
x2: f64::from(app.ball.left()),
|
||||
y2: f64::from(app.ball.top()),
|
||||
color: Color::Yellow,
|
||||
});
|
||||
})
|
||||
.x_bounds([10.0, 110.0])
|
||||
.y_bounds([10.0, 110.0])
|
||||
.render(&mut f, chunks[1]);
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
use util::*;
|
||||
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::layout::Rect;
|
||||
use termion::event::Key;
|
||||
use termion::input::MouseTerminal;
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::screen::AlternateScreen;
|
||||
use tui::backend::TermionBackend;
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Axis, Block, Borders, Chart, Dataset, Marker, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use crate::util::SinSignal;
|
||||
|
||||
struct App {
|
||||
size: Rect,
|
||||
signal1: SinSignal,
|
||||
data1: Vec<(f64, f64)>,
|
||||
signal2: SinSignal,
|
||||
@@ -34,7 +30,6 @@ impl App {
|
||||
let data1 = signal1.by_ref().take(200).collect::<Vec<(f64, f64)>>();
|
||||
let data2 = signal2.by_ref().take(200).collect::<Vec<(f64, f64)>>();
|
||||
App {
|
||||
size: Rect::default(),
|
||||
signal1,
|
||||
data1,
|
||||
signal2,
|
||||
@@ -43,7 +38,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
fn update(&mut self) {
|
||||
for _ in 0..5 {
|
||||
self.data1.remove(0);
|
||||
}
|
||||
@@ -57,111 +52,76 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
Tick,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn main() -> Result<(), failure::Error> {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
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()?;
|
||||
|
||||
// Channels
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
let clock_tx = tx.clone();
|
||||
|
||||
// Input
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Tick
|
||||
thread::spawn(move || loop {
|
||||
clock_tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
});
|
||||
let events = Events::new();
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if app.size != size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
terminal.draw(|mut f| {
|
||||
let size = f.size();
|
||||
Chart::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart")
|
||||
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD))
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels_style(Style::default().modifier(Modifier::ITALIC))
|
||||
.bounds(app.window)
|
||||
.labels(&[
|
||||
&format!("{}", app.window[0]),
|
||||
&format!("{}", (app.window[0] + app.window[1]) / 2.0),
|
||||
&format!("{}", app.window[1]),
|
||||
]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels_style(Style::default().modifier(Modifier::ITALIC))
|
||||
.bounds([-20.0, 20.0])
|
||||
.labels(&["-20", "0", "20"]),
|
||||
)
|
||||
.datasets(&[
|
||||
Dataset::default()
|
||||
.name("data2")
|
||||
.marker(Marker::Dot)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.data(&app.data1),
|
||||
Dataset::default()
|
||||
.name("data3")
|
||||
.marker(Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.data(&app.data2),
|
||||
])
|
||||
.render(&mut f, size);
|
||||
})?;
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => if input == event::Key::Char('q') {
|
||||
break;
|
||||
},
|
||||
match events.next()? {
|
||||
Event::Input(input) => {
|
||||
if input == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Tick => {
|
||||
app.advance();
|
||||
app.update();
|
||||
}
|
||||
}
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
Chart::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart")
|
||||
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::Bold))
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels_style(Style::default().modifier(Modifier::Italic))
|
||||
.bounds(app.window)
|
||||
.labels(&[
|
||||
&format!("{}", app.window[0]),
|
||||
&format!("{}", (app.window[0] + app.window[1]) / 2.0),
|
||||
&format!("{}", app.window[1]),
|
||||
]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels_style(Style::default().modifier(Modifier::Italic))
|
||||
.bounds([-20.0, 20.0])
|
||||
.labels(&["-20", "0", "20"]),
|
||||
)
|
||||
.datasets(&[
|
||||
Dataset::default()
|
||||
.name("data2")
|
||||
.marker(Marker::Dot)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.data(&app.data1),
|
||||
Dataset::default()
|
||||
.name("data3")
|
||||
.marker(Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.data(&app.data2),
|
||||
])
|
||||
.render(&mut f, app.size);
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
extern crate crossterm;
|
||||
extern crate tui;
|
||||
|
||||
use std::error::Error;
|
||||
use std::io;
|
||||
|
||||
use tui::backend::CrosstermBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Block, Borders, Paragraph, Text, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
fn main() {
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new()).unwrap();
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
draw(&mut terminal).unwrap();
|
||||
loop {
|
||||
{
|
||||
let input = crossterm::input(terminal.backend().screen());
|
||||
match input.read_char() {
|
||||
Ok(c) => if c == 'q' {
|
||||
break;
|
||||
},
|
||||
Err(e) => panic!("{}", e.description()),
|
||||
};
|
||||
}
|
||||
draw(&mut terminal).unwrap();
|
||||
}
|
||||
terminal.show_cursor().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<CrosstermBackend>) -> io::Result<()> {
|
||||
let size = t.size()?;
|
||||
t.draw(|mut f| {
|
||||
let text = [
|
||||
Text::Data("It "),
|
||||
Text::StyledData("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);
|
||||
})
|
||||
}
|
||||
93
examples/crossterm_demo.rs
Normal file
93
examples/crossterm_demo.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
#[allow(dead_code)]
|
||||
mod demo;
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crossterm;
|
||||
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 screen = crossterm::Screen::default();
|
||||
let alternate_screen = screen.enable_alternate_modes(true)?;
|
||||
let backend = CrosstermBackend::with_alternate_screen(alternate_screen)?;
|
||||
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(())
|
||||
}
|
||||
75
examples/curses_demo.rs
Normal file
75
examples/curses_demo.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
mod demo;
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use std::io;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use easycurses;
|
||||
use structopt::StructOpt;
|
||||
use tui::backend::CursesBackend;
|
||||
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 mut backend = CursesBackend::new().ok_or(io::Error::new(io::ErrorKind::Other, ""))?;
|
||||
let curses = backend.get_curses_mut();
|
||||
curses.set_echo(false);
|
||||
curses.set_input_timeout(easycurses::TimeoutMode::WaitUpTo(50));
|
||||
curses.set_input_mode(easycurses::InputMode::RawCharacter);
|
||||
curses.set_keypad_enabled(true);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
let mut app = App::new("Curses 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_mut().get_curses_mut().get_input() {
|
||||
Some(input) => {
|
||||
match input {
|
||||
easycurses::Input::Character(c) => {
|
||||
app.on_key(c);
|
||||
}
|
||||
easycurses::Input::KeyUp => {
|
||||
app.on_up();
|
||||
}
|
||||
easycurses::Input::KeyDown => {
|
||||
app.on_down();
|
||||
}
|
||||
easycurses::Input::KeyLeft => {
|
||||
app.on_left();
|
||||
}
|
||||
easycurses::Input::KeyRight => {
|
||||
app.on_right();
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
terminal.backend_mut().get_curses_mut().flush_input();
|
||||
if last_tick.elapsed() > tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
if app.should_quit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,12 +1,21 @@
|
||||
extern crate tui;
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use std::io;
|
||||
|
||||
use termion::event::Key;
|
||||
use termion::input::MouseTerminal;
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::screen::AlternateScreen;
|
||||
use tui::backend::TermionBackend;
|
||||
use tui::buffer::Buffer;
|
||||
use tui::layout::Rect;
|
||||
use tui::style::Style;
|
||||
use tui::widgets::Widget;
|
||||
use tui::Terminal;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
|
||||
struct Label<'a> {
|
||||
text: &'a str,
|
||||
}
|
||||
@@ -19,7 +28,7 @@ impl<'a> Default for Label<'a> {
|
||||
|
||||
impl<'a> Widget for Label<'a> {
|
||||
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_string(area.left(), area.top(), self.text, &Style::default());
|
||||
buf.set_string(area.left(), area.top(), self.text, Style::default());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,13 +39,31 @@ impl<'a> Label<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut terminal = Terminal::new(MouseBackend::new().unwrap()).unwrap();
|
||||
let size = terminal.size().unwrap();
|
||||
terminal.clear().unwrap();
|
||||
terminal
|
||||
.draw(|mut f| {
|
||||
fn main() -> Result<(), failure::Error> {
|
||||
// Terminal initialization
|
||||
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)?;
|
||||
|
||||
let events = Events::new();
|
||||
|
||||
loop {
|
||||
terminal.draw(|mut f| {
|
||||
let size = f.size();
|
||||
Label::default().text("Test").render(&mut f, size);
|
||||
})
|
||||
.unwrap();
|
||||
})?;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(key) => {
|
||||
if key == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
522
examples/demo.rs
522
examples/demo.rs
@@ -1,522 +0,0 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate stderrlog;
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
|
||||
mod util;
|
||||
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::canvas::{Canvas, Line, Map, MapResolution};
|
||||
use tui::widgets::{
|
||||
Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, Item, List, Marker, Paragraph, Row,
|
||||
SelectableList, Sparkline, Table, Tabs, Text, Widget,
|
||||
};
|
||||
use tui::{Frame, Terminal};
|
||||
|
||||
use util::*;
|
||||
|
||||
struct Server<'a> {
|
||||
name: &'a str,
|
||||
location: &'a str,
|
||||
coords: (f64, f64),
|
||||
status: &'a str,
|
||||
}
|
||||
|
||||
struct App<'a> {
|
||||
size: Rect,
|
||||
items: Vec<&'a str>,
|
||||
events: Vec<(&'a str, &'a str)>,
|
||||
selected: usize,
|
||||
tabs: MyTabs<'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>>,
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
Tick,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
stderrlog::new()
|
||||
.module(module_path!())
|
||||
.verbosity(4)
|
||||
.init()
|
||||
.unwrap();
|
||||
info!("Start");
|
||||
|
||||
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 {
|
||||
size: Rect::default(),
|
||||
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: MyTabs {
|
||||
titles: vec!["Tab0", "Tab1"],
|
||||
selection: 0,
|
||||
},
|
||||
show_chart: true,
|
||||
progress: 0,
|
||||
data: rand_signal.clone().take(300).collect(),
|
||||
data2: sin_signal.clone().take(100).collect(),
|
||||
data3: sin_signal2.clone().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",
|
||||
},
|
||||
],
|
||||
};
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
|
||||
for _ in 0..100 {
|
||||
sin_signal.next();
|
||||
}
|
||||
for _ in 0..200 {
|
||||
sin_signal2.next();
|
||||
}
|
||||
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
thread::spawn(move || {
|
||||
let tx = tx.clone();
|
||||
loop {
|
||||
tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(time::Duration::from_millis(250));
|
||||
}
|
||||
});
|
||||
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => match input {
|
||||
event::Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
event::Key::Up => {
|
||||
if app.selected > 0 {
|
||||
app.selected -= 1
|
||||
};
|
||||
}
|
||||
event::Key::Down => if app.selected < app.items.len() - 1 {
|
||||
app.selected += 1;
|
||||
},
|
||||
event::Key::Left => {
|
||||
app.tabs.previous();
|
||||
}
|
||||
event::Key::Right => {
|
||||
app.tabs.next();
|
||||
}
|
||||
event::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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
terminal.show_cursor().unwrap();
|
||||
terminal.clear().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
||||
.split(app.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.selection)
|
||||
.render(&mut f, chunks[0]);
|
||||
match app.tabs.selection {
|
||||
0 => {
|
||||
draw_first_tab(&mut f, app, chunks[1]);
|
||||
}
|
||||
1 => {
|
||||
draw_second_tab(&mut f, app, chunks[1]);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
fn draw_first_tab(f: &mut Frame<MouseBackend>, app: &App, area: Rect) {
|
||||
let chunks = Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(7),
|
||||
Constraint::Min(7),
|
||||
Constraint::Length(7),
|
||||
].as_ref(),
|
||||
)
|
||||
.split(area);
|
||||
draw_gauges(f, app, chunks[0]);
|
||||
draw_charts(f, app, chunks[1]);
|
||||
draw_text(f, chunks[2]);
|
||||
}
|
||||
|
||||
fn draw_gauges(f: &mut Frame<MouseBackend>, app: &App, area: Rect) {
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Length(2), Constraint::Length(3)].as_ref())
|
||||
.margin(1)
|
||||
.split(area);
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Graphs")
|
||||
.render(f, area);
|
||||
Gauge::default()
|
||||
.block(Block::default().title("Gauge:"))
|
||||
.style(
|
||||
Style::default()
|
||||
.fg(Color::Magenta)
|
||||
.bg(Color::Black)
|
||||
.modifier(Modifier::Italic),
|
||||
)
|
||||
.label(&format!("{} / 100", app.progress))
|
||||
.percent(app.progress)
|
||||
.render(f, chunks[0]);
|
||||
Sparkline::default()
|
||||
.block(Block::default().title("Sparkline:"))
|
||||
.style(Style::default().fg(Color::Green))
|
||||
.data(&app.data)
|
||||
.render(f, chunks[1]);
|
||||
}
|
||||
|
||||
fn draw_charts(f: &mut Frame<MouseBackend>, app: &App, area: Rect) {
|
||||
let constraints = if app.show_chart {
|
||||
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
|
||||
} else {
|
||||
vec![Constraint::Percentage(100)]
|
||||
};
|
||||
let chunks = Layout::default()
|
||||
.constraints(constraints)
|
||||
.direction(Direction::Horizontal)
|
||||
.split(area);
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[0]);
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.direction(Direction::Horizontal)
|
||||
.split(chunks[0]);
|
||||
SelectableList::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.items(&app.items)
|
||||
.select(Some(app.selected))
|
||||
.highlight_style(Style::default().fg(Color::Yellow).modifier(Modifier::Bold))
|
||||
.highlight_symbol(">")
|
||||
.render(f, chunks[0]);
|
||||
let info_style = Style::default().fg(Color::White);
|
||||
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)| {
|
||||
Item::StyledData(
|
||||
format!("{}: {}", level, evt),
|
||||
match level {
|
||||
"ERROR" => &error_style,
|
||||
"CRITICAL" => &critical_style,
|
||||
"WARNING" => &warning_style,
|
||||
_ => &info_style,
|
||||
},
|
||||
)
|
||||
});
|
||||
List::new(events)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.render(f, chunks[1]);
|
||||
}
|
||||
BarChart::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Bar chart"))
|
||||
.data(&app.data4)
|
||||
.bar_width(3)
|
||||
.bar_gap(2)
|
||||
.value_style(
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Green)
|
||||
.modifier(Modifier::Italic),
|
||||
)
|
||||
.label_style(Style::default().fg(Color::Yellow))
|
||||
.style(Style::default().fg(Color::Green))
|
||||
.render(f, chunks[1]);
|
||||
}
|
||||
if app.show_chart {
|
||||
Chart::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart")
|
||||
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::Bold))
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels_style(Style::default().modifier(Modifier::Italic))
|
||||
.bounds(app.window)
|
||||
.labels(&[
|
||||
&format!("{}", app.window[0]),
|
||||
&format!("{}", (app.window[0] + app.window[1]) / 2.0),
|
||||
&format!("{}", app.window[1]),
|
||||
]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels_style(Style::default().modifier(Modifier::Italic))
|
||||
.bounds([-20.0, 20.0])
|
||||
.labels(&["-20", "0", "20"]),
|
||||
)
|
||||
.datasets(&[
|
||||
Dataset::default()
|
||||
.name("data2")
|
||||
.marker(Marker::Dot)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.data(&app.data2),
|
||||
Dataset::default()
|
||||
.name("data3")
|
||||
.marker(Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.data(&app.data3),
|
||||
])
|
||||
.render(f, chunks[1]);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_text(f: &mut Frame<MouseBackend>, area: Rect) {
|
||||
let text = [
|
||||
Text::Data("This is a paragraph with several lines. You can change style your text the way you want.\n\nFox example: "),
|
||||
Text::StyledData("under", Style::default().fg(Color::Red)),
|
||||
Text::Data(" "),
|
||||
Text::StyledData("the", Style::default().fg(Color::Green)),
|
||||
Text::Data(" "),
|
||||
Text::StyledData("rainbow", Style::default().fg(Color::Blue)),
|
||||
Text::Data(".\nOh and if you didn't "),
|
||||
Text::StyledData("notice", Style::default().modifier(Modifier::Italic)),
|
||||
Text::Data(" you can "),
|
||||
Text::StyledData("automatically", Style::default().modifier(Modifier::Bold)),
|
||||
Text::Data(" "),
|
||||
Text::StyledData("wrap", Style::default().modifier(Modifier::Invert)),
|
||||
Text::Data(" your "),
|
||||
Text::StyledData("text", Style::default().modifier(Modifier::Underline)),
|
||||
Text::Data(".\nOne more thing is that it should display unicode characters: 10€")
|
||||
];
|
||||
Paragraph::new(text.iter())
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Footer")
|
||||
.title_style(Style::default().fg(Color::Magenta).modifier(Modifier::Bold)),
|
||||
)
|
||||
.wrap(true)
|
||||
.render(f, area);
|
||||
}
|
||||
|
||||
fn draw_second_tab(f: &mut Frame<MouseBackend>, app: &App, area: Rect) {
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)].as_ref())
|
||||
.direction(Direction::Horizontal)
|
||||
.split(area);
|
||||
let up_style = Style::default().fg(Color::Green);
|
||||
let failure_style = Style::default().fg(Color::Red);
|
||||
Table::new(
|
||||
["Server", "Location", "Status"].into_iter(),
|
||||
app.servers.iter().map(|s| {
|
||||
let style = if s.status == "Up" {
|
||||
&up_style
|
||||
} else {
|
||||
&failure_style
|
||||
};
|
||||
Row::StyledData(vec![s.name, s.location, s.status].into_iter(), style)
|
||||
}),
|
||||
).block(Block::default().title("Servers").borders(Borders::ALL))
|
||||
.header_style(Style::default().fg(Color::Yellow))
|
||||
.widths(&[15, 15, 10])
|
||||
.render(f, chunks[0]);
|
||||
|
||||
Canvas::default()
|
||||
.block(Block::default().title("World").borders(Borders::ALL))
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
color: Color::White,
|
||||
resolution: MapResolution::High,
|
||||
});
|
||||
ctx.layer();
|
||||
for (i, s1) in app.servers.iter().enumerate() {
|
||||
for s2 in &app.servers[i + 1..] {
|
||||
ctx.draw(&Line {
|
||||
x1: s1.coords.1,
|
||||
y1: s1.coords.0,
|
||||
y2: s2.coords.0,
|
||||
x2: s2.coords.1,
|
||||
color: Color::Yellow,
|
||||
});
|
||||
}
|
||||
}
|
||||
for server in &app.servers {
|
||||
let color = if server.status == "Up" {
|
||||
Color::Green
|
||||
} else {
|
||||
Color::Red
|
||||
};
|
||||
ctx.print(server.coords.1, server.coords.0, "X", color);
|
||||
}
|
||||
})
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0])
|
||||
.render(f, chunks[1]);
|
||||
}
|
||||
249
examples/demo/app.rs
Normal file
249
examples/demo/app.rs
Normal 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
3
examples/demo/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod app;
|
||||
pub mod ui;
|
||||
pub use app::App;
|
||||
290
examples/demo/ui.rs
Normal file
290
examples/demo/ui.rs
Normal file
@@ -0,0 +1,290 @@
|
||||
use std::io;
|
||||
|
||||
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};
|
||||
use tui::widgets::{
|
||||
Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, List, Marker, Paragraph, Row,
|
||||
SelectableList, Sparkline, Table, Tabs, Text, Widget,
|
||||
};
|
||||
use tui::{Frame, Terminal};
|
||||
|
||||
use crate::demo::App;
|
||||
|
||||
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)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(7),
|
||||
Constraint::Min(7),
|
||||
Constraint::Length(7),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(area);
|
||||
draw_gauges(f, app, chunks[0]);
|
||||
draw_charts(f, app, chunks[1]);
|
||||
draw_text(f, chunks[2]);
|
||||
}
|
||||
|
||||
fn draw_gauges<B>(f: &mut Frame<B>, app: &App, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Length(2), Constraint::Length(3)].as_ref())
|
||||
.margin(1)
|
||||
.split(area);
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Graphs")
|
||||
.render(f, area);
|
||||
Gauge::default()
|
||||
.block(Block::default().title("Gauge:"))
|
||||
.style(
|
||||
Style::default()
|
||||
.fg(Color::Magenta)
|
||||
.bg(Color::Black)
|
||||
.modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
)
|
||||
.label(&format!("{} / 100", app.progress))
|
||||
.percent(app.progress)
|
||||
.render(f, chunks[0]);
|
||||
Sparkline::default()
|
||||
.block(Block::default().title("Sparkline:"))
|
||||
.style(Style::default().fg(Color::Green))
|
||||
.data(&app.sparkline.points)
|
||||
.render(f, chunks[1]);
|
||||
}
|
||||
|
||||
fn draw_charts<B>(f: &mut Frame<B>, app: &App, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let constraints = if app.show_chart {
|
||||
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
|
||||
} else {
|
||||
vec![Constraint::Percentage(100)]
|
||||
};
|
||||
let chunks = Layout::default()
|
||||
.constraints(constraints)
|
||||
.direction(Direction::Horizontal)
|
||||
.split(area);
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[0]);
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.direction(Direction::Horizontal)
|
||||
.split(chunks[0]);
|
||||
SelectableList::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.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]);
|
||||
let info_style = Style::default().fg(Color::White);
|
||||
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.logs.items.iter().map(|&(evt, level)| {
|
||||
Text::styled(
|
||||
format!("{}: {}", level, evt),
|
||||
match level {
|
||||
"ERROR" => error_style,
|
||||
"CRITICAL" => critical_style,
|
||||
"WARNING" => warning_style,
|
||||
_ => info_style,
|
||||
},
|
||||
)
|
||||
});
|
||||
List::new(events)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.render(f, chunks[1]);
|
||||
}
|
||||
BarChart::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Bar chart"))
|
||||
.data(&app.barchart)
|
||||
.bar_width(3)
|
||||
.bar_gap(2)
|
||||
.value_style(
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Green)
|
||||
.modifier(Modifier::ITALIC),
|
||||
)
|
||||
.label_style(Style::default().fg(Color::Yellow))
|
||||
.style(Style::default().fg(Color::Green))
|
||||
.render(f, chunks[1]);
|
||||
}
|
||||
if app.show_chart {
|
||||
Chart::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart")
|
||||
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD))
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels_style(Style::default().modifier(Modifier::ITALIC))
|
||||
.bounds(app.signals.window)
|
||||
.labels(&[
|
||||
&format!("{}", app.signals.window[0]),
|
||||
&format!("{}", (app.signals.window[0] + app.signals.window[1]) / 2.0),
|
||||
&format!("{}", app.signals.window[1]),
|
||||
]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels_style(Style::default().modifier(Modifier::ITALIC))
|
||||
.bounds([-20.0, 20.0])
|
||||
.labels(&["-20", "0", "20"]),
|
||||
)
|
||||
.datasets(&[
|
||||
Dataset::default()
|
||||
.name("data2")
|
||||
.marker(Marker::Dot)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.data(&app.signals.sin1.points),
|
||||
Dataset::default()
|
||||
.name("data3")
|
||||
.marker(Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.data(&app.signals.sin2.points),
|
||||
])
|
||||
.render(f, chunks[1]);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_text<B>(f: &mut Frame<B>, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let text = [
|
||||
Text::raw("This is a paragraph with several lines. You can change style your text the way you want.\n\nFox example: "),
|
||||
Text::styled("under", Style::default().fg(Color::Red)),
|
||||
Text::raw(" "),
|
||||
Text::styled("the", Style::default().fg(Color::Green)),
|
||||
Text::raw(" "),
|
||||
Text::styled("rainbow", Style::default().fg(Color::Blue)),
|
||||
Text::raw(".\nOh and if you didn't "),
|
||||
Text::styled("notice", Style::default().modifier(Modifier::ITALIC)),
|
||||
Text::raw(" you can "),
|
||||
Text::styled("automatically", Style::default().modifier(Modifier::BOLD)),
|
||||
Text::raw(" "),
|
||||
Text::styled("wrap", Style::default().modifier(Modifier::REVERSED)),
|
||||
Text::raw(" your "),
|
||||
Text::styled("text", Style::default().modifier(Modifier::UNDERLINED)),
|
||||
Text::raw(".\nOne more thing is that it should display unicode characters: 10€")
|
||||
];
|
||||
Paragraph::new(text.iter())
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Footer")
|
||||
.title_style(Style::default().fg(Color::Magenta).modifier(Modifier::BOLD)),
|
||||
)
|
||||
.wrap(true)
|
||||
.render(f, area);
|
||||
}
|
||||
|
||||
fn draw_second_tab<B>(f: &mut Frame<B>, app: &App, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)].as_ref())
|
||||
.direction(Direction::Horizontal)
|
||||
.split(area);
|
||||
let up_style = Style::default().fg(Color::Green);
|
||||
let failure_style = Style::default()
|
||||
.fg(Color::Red)
|
||||
.modifier(Modifier::RAPID_BLINK | Modifier::CROSSED_OUT);
|
||||
let header = ["Server", "Location", "Status"];
|
||||
let rows = app.servers.iter().map(|s| {
|
||||
let style = if s.status == "Up" {
|
||||
up_style
|
||||
} else {
|
||||
failure_style
|
||||
};
|
||||
Row::StyledData(vec![s.name, s.location, s.status].into_iter(), style)
|
||||
});
|
||||
Table::new(header.into_iter(), rows)
|
||||
.block(Block::default().title("Servers").borders(Borders::ALL))
|
||||
.header_style(Style::default().fg(Color::Yellow))
|
||||
.widths(&[15, 15, 10])
|
||||
.render(f, chunks[0]);
|
||||
|
||||
Canvas::default()
|
||||
.block(Block::default().title("World").borders(Borders::ALL))
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
color: Color::White,
|
||||
resolution: MapResolution::High,
|
||||
});
|
||||
ctx.layer();
|
||||
ctx.draw(&Rectangle {
|
||||
rect: Rect {
|
||||
x: 0,
|
||||
y: 30,
|
||||
width: 10,
|
||||
height: 10,
|
||||
},
|
||||
color: Color::Yellow,
|
||||
});
|
||||
for (i, s1) in app.servers.iter().enumerate() {
|
||||
for s2 in &app.servers[i + 1..] {
|
||||
ctx.draw(&Line {
|
||||
x1: s1.coords.1,
|
||||
y1: s1.coords.0,
|
||||
y2: s2.coords.0,
|
||||
x2: s2.coords.1,
|
||||
color: Color::Yellow,
|
||||
});
|
||||
}
|
||||
}
|
||||
for server in &app.servers {
|
||||
let color = if server.status == "Up" {
|
||||
Color::Green
|
||||
} else {
|
||||
Color::Red
|
||||
};
|
||||
ctx.print(server.coords.1, server.coords.0, "X", color);
|
||||
}
|
||||
})
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0])
|
||||
.render(f, chunks[1]);
|
||||
}
|
||||
@@ -1,40 +1,38 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use termion::event::Key;
|
||||
use termion::input::MouseTerminal;
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::screen::AlternateScreen;
|
||||
use tui::backend::TermionBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Block, Borders, Gauge, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
|
||||
struct App {
|
||||
size: Rect,
|
||||
progress1: u16,
|
||||
progress2: u16,
|
||||
progress3: u16,
|
||||
progress3: f64,
|
||||
progress4: u16,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App {
|
||||
size: Rect::default(),
|
||||
progress1: 0,
|
||||
progress2: 0,
|
||||
progress3: 0,
|
||||
progress3: 0.0,
|
||||
progress4: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
fn update(&mut self) {
|
||||
self.progress1 += 5;
|
||||
if self.progress1 > 100 {
|
||||
self.progress1 = 0;
|
||||
@@ -43,9 +41,9 @@ impl App {
|
||||
if self.progress2 > 100 {
|
||||
self.progress2 = 0;
|
||||
}
|
||||
self.progress3 += 1;
|
||||
if self.progress3 > 100 {
|
||||
self.progress3 = 0;
|
||||
self.progress3 += 0.001;
|
||||
if self.progress3 > 1.0 {
|
||||
self.progress3 = 0.0;
|
||||
}
|
||||
self.progress4 += 3;
|
||||
if self.progress4 > 100 {
|
||||
@@ -54,105 +52,70 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
Tick,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn main() -> Result<(), failure::Error> {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
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()?;
|
||||
|
||||
// Channels
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
let clock_tx = tx.clone();
|
||||
let events = Events::new();
|
||||
|
||||
// Input
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Tick
|
||||
thread::spawn(move || loop {
|
||||
clock_tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
});
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
terminal.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => if input == event::Key::Char('q') {
|
||||
break;
|
||||
},
|
||||
Gauge::default()
|
||||
.block(Block::default().title("Gauge1").borders(Borders::ALL))
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.percent(app.progress1)
|
||||
.render(&mut f, chunks[0]);
|
||||
Gauge::default()
|
||||
.block(Block::default().title("Gauge2").borders(Borders::ALL))
|
||||
.style(Style::default().fg(Color::Magenta).bg(Color::Green))
|
||||
.percent(app.progress2)
|
||||
.label(&format!("{}/100", app.progress2))
|
||||
.render(&mut f, chunks[1]);
|
||||
Gauge::default()
|
||||
.block(Block::default().title("Gauge3").borders(Borders::ALL))
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.ratio(app.progress3)
|
||||
.render(&mut f, chunks[2]);
|
||||
Gauge::default()
|
||||
.block(Block::default().title("Gauge4").borders(Borders::ALL))
|
||||
.style(Style::default().fg(Color::Cyan).modifier(Modifier::ITALIC))
|
||||
.percent(app.progress4)
|
||||
.label(&format!("{}/100", app.progress2))
|
||||
.render(&mut f, chunks[3]);
|
||||
})?;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(input) => {
|
||||
if input == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Tick => {
|
||||
app.advance();
|
||||
app.update();
|
||||
}
|
||||
}
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
].as_ref(),
|
||||
)
|
||||
.split(app.size);
|
||||
Gauge::default()
|
||||
.block(Block::default().title("Gauge1").borders(Borders::ALL))
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.percent(app.progress1)
|
||||
.render(&mut f, chunks[0]);
|
||||
Gauge::default()
|
||||
.block(Block::default().title("Gauge2").borders(Borders::ALL))
|
||||
.style(Style::default().fg(Color::Magenta).bg(Color::Green))
|
||||
.percent(app.progress2)
|
||||
.label(&format!("{}/100", app.progress2))
|
||||
.render(&mut f, chunks[1]);
|
||||
Gauge::default()
|
||||
.block(Block::default().title("Gauge2").borders(Borders::ALL))
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.percent(app.progress3)
|
||||
.render(&mut f, chunks[2]);
|
||||
Gauge::default()
|
||||
.block(Block::default().title("Gauge3").borders(Borders::ALL))
|
||||
.style(Style::default().fg(Color::Cyan).modifier(Modifier::Italic))
|
||||
.percent(app.progress4)
|
||||
.label(&format!("{}/100", app.progress2))
|
||||
.render(&mut f, chunks[3]);
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,107 +1,63 @@
|
||||
extern crate log;
|
||||
extern crate stderrlog;
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use termion::event::Key;
|
||||
use termion::input::MouseTerminal;
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::screen::AlternateScreen;
|
||||
use tui::backend::TermionBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout};
|
||||
use tui::widgets::{Block, Borders, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
struct App {
|
||||
size: Rect,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App {
|
||||
size: Rect::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
}
|
||||
|
||||
fn main() {
|
||||
stderrlog::new().verbosity(4).init().unwrap();
|
||||
use crate::util::event::{Event, Events};
|
||||
|
||||
fn main() -> Result<(), failure::Error> {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
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()?;
|
||||
|
||||
// Channels
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
|
||||
// Input
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
let events = Events::new();
|
||||
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
terminal.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(80),
|
||||
Constraint::Percentage(10),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => if let event::Key::Char('q') = input {
|
||||
break;
|
||||
},
|
||||
Block::default()
|
||||
.title("Block")
|
||||
.borders(Borders::ALL)
|
||||
.render(&mut f, chunks[0]);
|
||||
Block::default()
|
||||
.title("Block 2")
|
||||
.borders(Borders::ALL)
|
||||
.render(&mut f, chunks[2]);
|
||||
})?;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(input) => {
|
||||
if let Key::Char('q') = input {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(80),
|
||||
Constraint::Percentage(10),
|
||||
].as_ref(),
|
||||
)
|
||||
.split(app.size);
|
||||
|
||||
Block::default()
|
||||
.title("Block")
|
||||
.borders(Borders::ALL)
|
||||
.render(&mut f, chunks[0]);
|
||||
Block::default()
|
||||
.title("Block 2")
|
||||
.borders(Borders::ALL)
|
||||
.render(&mut f, chunks[2]);
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
157
examples/list.rs
157
examples/list.rs
@@ -1,22 +1,21 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::layout::{Constraint, Corner, Direction, Layout, Rect};
|
||||
use termion::event::Key;
|
||||
use termion::input::MouseTerminal;
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::screen::AlternateScreen;
|
||||
use tui::backend::TermionBackend;
|
||||
use tui::layout::{Constraint, Corner, Direction, Layout};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Block, Borders, Item, List, SelectableList, Widget};
|
||||
use tui::widgets::{Block, Borders, List, SelectableList, Text, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
|
||||
struct App<'a> {
|
||||
size: Rect,
|
||||
items: Vec<&'a str>,
|
||||
selected: Option<usize>,
|
||||
events: Vec<(&'a str, &'a str)>,
|
||||
@@ -29,7 +28,6 @@ struct App<'a> {
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
size: Rect::default(),
|
||||
items: vec![
|
||||
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9",
|
||||
"Item10", "Item11", "Item12", "Item13", "Item14", "Item15", "Item16", "Item17",
|
||||
@@ -77,65 +75,64 @@ impl<'a> App<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
Tick,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn main() -> Result<(), failure::Error> {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
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()?;
|
||||
|
||||
// Channels
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
let clock_tx = tx.clone();
|
||||
|
||||
// Input
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Tick
|
||||
thread::spawn(move || loop {
|
||||
clock_tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
});
|
||||
let events = Events::new();
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
terminal.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
let style = Style::default().fg(Color::Black).bg(Color::White);
|
||||
SelectableList::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.items(&app.items)
|
||||
.select(app.selected)
|
||||
.style(style)
|
||||
.highlight_style(style.fg(Color::LightGreen).modifier(Modifier::BOLD))
|
||||
.highlight_symbol(">")
|
||||
.render(&mut f, chunks[0]);
|
||||
{
|
||||
let events = app.events.iter().map(|&(evt, level)| {
|
||||
Text::styled(
|
||||
format!("{}: {}", level, evt),
|
||||
match level {
|
||||
"ERROR" => app.error_style,
|
||||
"CRITICAL" => app.critical_style,
|
||||
"WARNING" => app.warning_style,
|
||||
_ => app.info_style,
|
||||
},
|
||||
)
|
||||
});
|
||||
List::new(events)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.start_corner(Corner::BottomLeft)
|
||||
.render(&mut f, chunks[1]);
|
||||
}
|
||||
})?;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(input) => match input {
|
||||
event::Key::Char('q') => {
|
||||
Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
event::Key::Left => {
|
||||
Key::Left => {
|
||||
app.selected = None;
|
||||
}
|
||||
event::Key::Down => {
|
||||
Key::Down => {
|
||||
app.selected = if let Some(selected) = app.selected {
|
||||
if selected >= app.items.len() - 1 {
|
||||
Some(0)
|
||||
@@ -146,7 +143,7 @@ fn main() {
|
||||
Some(0)
|
||||
}
|
||||
}
|
||||
event::Key::Up => {
|
||||
Key::Up => {
|
||||
app.selected = if let Some(selected) = app.selected {
|
||||
if selected > 0 {
|
||||
Some(selected - 1)
|
||||
@@ -163,45 +160,7 @@ fn main() {
|
||||
app.advance();
|
||||
}
|
||||
}
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
terminal.clear().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(app.size);
|
||||
|
||||
let style = Style::default().fg(Color::Black).bg(Color::White);
|
||||
SelectableList::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.items(&app.items)
|
||||
.select(app.selected)
|
||||
.style(style)
|
||||
.highlight_style(style.fg(Color::LightGreen).modifier(Modifier::Bold))
|
||||
.highlight_symbol(">")
|
||||
.render(&mut f, chunks[0]);
|
||||
{
|
||||
let events = app.events.iter().map(|&(evt, level)| {
|
||||
Item::StyledData(
|
||||
format!("{}: {}", level, evt),
|
||||
match level {
|
||||
"ERROR" => &app.error_style,
|
||||
"CRITICAL" => &app.critical_style,
|
||||
"WARNING" => &app.warning_style,
|
||||
_ => &app.info_style,
|
||||
},
|
||||
)
|
||||
});
|
||||
List::new(events)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.start_corner(Corner::BottomLeft)
|
||||
.render(&mut f, chunks[1]);
|
||||
}
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,83 +1,110 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use std::io;
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
||||
use termion::event::Key;
|
||||
use termion::input::MouseTerminal;
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::screen::AlternateScreen;
|
||||
use tui::backend::TermionBackend;
|
||||
use tui::layout::{Alignment, Constraint, Direction, Layout};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Block, Paragraph, Text, Widget};
|
||||
use tui::widgets::{Block, Borders, Paragraph, Text, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
fn main() {
|
||||
let mut terminal = Terminal::new(MouseBackend::new().unwrap()).unwrap();
|
||||
let stdin = io::stdin();
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
use crate::util::event::{Event, Events};
|
||||
|
||||
let mut term_size = terminal.size().unwrap();
|
||||
draw(&mut terminal, term_size).unwrap();
|
||||
fn main() -> Result<(), failure::Error> {
|
||||
// Terminal initialization
|
||||
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()?;
|
||||
|
||||
for c in stdin.keys() {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != term_size {
|
||||
terminal.resize(size).unwrap();
|
||||
term_size = size;
|
||||
}
|
||||
let events = Events::new();
|
||||
|
||||
draw(&mut terminal, term_size).unwrap();
|
||||
let evt = c.unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
let mut scroll: u16 = 0;
|
||||
loop {
|
||||
terminal.draw(|mut f| {
|
||||
let size = f.size();
|
||||
|
||||
// Words made "loooong" to demonstrate line breaking.
|
||||
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
|
||||
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
|
||||
long_line.push('\n');
|
||||
|
||||
Block::default()
|
||||
.style(Style::default().bg(Color::White))
|
||||
.render(&mut f, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(5)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(size);
|
||||
|
||||
let text = [
|
||||
Text::raw("This is a line \n"),
|
||||
Text::styled("This is a line \n", Style::default().fg(Color::Red)),
|
||||
Text::styled("This is a line\n", Style::default().bg(Color::Blue)),
|
||||
Text::styled(
|
||||
"This is a longer line\n",
|
||||
Style::default().modifier(Modifier::CROSSED_OUT),
|
||||
),
|
||||
Text::styled(&long_line, Style::default().bg(Color::Green)),
|
||||
Text::styled(
|
||||
"This is a line\n",
|
||||
Style::default().fg(Color::Green).modifier(Modifier::ITALIC),
|
||||
),
|
||||
];
|
||||
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title_style(Style::default().modifier(Modifier::BOLD));
|
||||
Paragraph::new(text.iter())
|
||||
.block(block.clone().title("Left, no wrap"))
|
||||
.alignment(Alignment::Left)
|
||||
.render(&mut f, chunks[0]);
|
||||
Paragraph::new(text.iter())
|
||||
.block(block.clone().title("Left, wrap"))
|
||||
.alignment(Alignment::Left)
|
||||
.wrap(true)
|
||||
.render(&mut f, chunks[1]);
|
||||
Paragraph::new(text.iter())
|
||||
.block(block.clone().title("Center, wrap"))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(true)
|
||||
.scroll(scroll)
|
||||
.render(&mut f, chunks[2]);
|
||||
Paragraph::new(text.iter())
|
||||
.block(block.clone().title("Right, wrap"))
|
||||
.alignment(Alignment::Right)
|
||||
.wrap(true)
|
||||
.render(&mut f, chunks[3]);
|
||||
})?;
|
||||
|
||||
scroll += 1;
|
||||
scroll %= 10;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(key) => {
|
||||
if key == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
terminal.show_cursor().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, size: Rect) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
Block::default()
|
||||
.style(Style::default().bg(Color::White))
|
||||
.render(&mut f, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(5)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(30),
|
||||
].as_ref(),
|
||||
)
|
||||
.split(size);
|
||||
|
||||
let text = [
|
||||
Text::Data("This a line\n"),
|
||||
Text::StyledData("This a line\n", Style::default().fg(Color::Red)),
|
||||
Text::StyledData("This a line\n", Style::default().bg(Color::Blue)),
|
||||
Text::StyledData(
|
||||
"This a longer line\n",
|
||||
Style::default().modifier(Modifier::CrossedOut),
|
||||
),
|
||||
Text::StyledData(
|
||||
"This a line\n",
|
||||
Style::default().fg(Color::Green).modifier(Modifier::Italic),
|
||||
),
|
||||
];
|
||||
|
||||
Paragraph::new(text.iter())
|
||||
.alignment(Alignment::Left)
|
||||
.render(&mut f, chunks[0]);
|
||||
Paragraph::new(text.iter())
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(true)
|
||||
.render(&mut f, chunks[1]);
|
||||
Paragraph::new(text.iter())
|
||||
.alignment(Alignment::Right)
|
||||
.wrap(true)
|
||||
.render(&mut f, chunks[2]);
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
extern crate rustbox;
|
||||
extern crate tui;
|
||||
|
||||
use rustbox::Key;
|
||||
use std::error::Error;
|
||||
|
||||
use tui::backend::RustboxBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Block, Borders, Paragraph, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
fn main() {
|
||||
let mut terminal = Terminal::new(RustboxBackend::new().unwrap()).unwrap();
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
draw(&mut terminal);
|
||||
loop {
|
||||
match terminal.backend().rustbox().poll_event(false) {
|
||||
Ok(rustbox::Event::KeyEvent(key)) => if key == Key::Char('q') {
|
||||
break;
|
||||
},
|
||||
Err(e) => panic!("{}", e.description()),
|
||||
_ => {}
|
||||
};
|
||||
draw(&mut terminal);
|
||||
}
|
||||
terminal.show_cursor().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<RustboxBackend>) {
|
||||
let size = t.size().unwrap();
|
||||
{
|
||||
let mut f = t.get_frame();
|
||||
Paragraph::default()
|
||||
.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)),
|
||||
)
|
||||
.text("It {yellow works}!")
|
||||
.render(&mut f, &size);
|
||||
}
|
||||
|
||||
t.draw().unwrap();
|
||||
}
|
||||
66
examples/rustbox_demo.rs
Normal file
66
examples/rustbox_demo.rs
Normal 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(())
|
||||
}
|
||||
@@ -1,25 +1,22 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
use util::*;
|
||||
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use termion::event::Key;
|
||||
use termion::input::MouseTerminal;
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::screen::AlternateScreen;
|
||||
use tui::backend::TermionBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout};
|
||||
use tui::style::{Color, Style};
|
||||
use tui::widgets::{Block, Borders, Sparkline, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use crate::util::RandomSignal;
|
||||
|
||||
struct App {
|
||||
size: Rect,
|
||||
signal: RandomSignal,
|
||||
data1: Vec<u64>,
|
||||
data2: Vec<u64>,
|
||||
@@ -33,7 +30,6 @@ impl App {
|
||||
let data2 = signal.by_ref().take(200).collect::<Vec<u64>>();
|
||||
let data3 = signal.by_ref().take(200).collect::<Vec<u64>>();
|
||||
App {
|
||||
size: Rect::default(),
|
||||
signal,
|
||||
data1,
|
||||
data2,
|
||||
@@ -41,7 +37,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
fn update(&mut self) {
|
||||
let value = self.signal.next().unwrap();
|
||||
self.data1.pop();
|
||||
self.data1.insert(0, value);
|
||||
@@ -54,111 +50,77 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
Tick,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn main() -> Result<(), failure::Error> {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
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()?;
|
||||
|
||||
// Channels
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
let clock_tx = tx.clone();
|
||||
// Setup event handlers
|
||||
let events = Events::new();
|
||||
|
||||
// Input
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Tick
|
||||
thread::spawn(move || loop {
|
||||
clock_tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
});
|
||||
|
||||
// App
|
||||
// Create default app state
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
terminal.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(7),
|
||||
Constraint::Min(0),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data1")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data1)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.render(&mut f, chunks[0]);
|
||||
Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data2")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data2)
|
||||
.style(Style::default().bg(Color::Green))
|
||||
.render(&mut f, chunks[1]);
|
||||
// Multiline
|
||||
Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data3")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data3)
|
||||
.style(Style::default().fg(Color::Red))
|
||||
.render(&mut f, chunks[2]);
|
||||
})?;
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => if input == event::Key::Char('q') {
|
||||
break;
|
||||
},
|
||||
match events.next()? {
|
||||
Event::Input(input) => {
|
||||
if input == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Tick => {
|
||||
app.advance();
|
||||
app.update();
|
||||
}
|
||||
}
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(7),
|
||||
Constraint::Min(0),
|
||||
].as_ref(),
|
||||
)
|
||||
.split(app.size);
|
||||
Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data1")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data1)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.render(&mut f, chunks[0]);
|
||||
Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data2")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data2)
|
||||
.style(Style::default().bg(Color::Green))
|
||||
.render(&mut f, chunks[1]);
|
||||
// Multiline
|
||||
Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data3")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data3)
|
||||
.style(Style::default().fg(Color::Red))
|
||||
.render(&mut f, chunks[2]);
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use std::io;
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::layout::{Constraint, Layout, Rect};
|
||||
use termion::event::Key;
|
||||
use termion::input::MouseTerminal;
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::screen::AlternateScreen;
|
||||
use tui::backend::TermionBackend;
|
||||
use tui::layout::{Constraint, Layout};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Block, Borders, Row, Table, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
|
||||
struct App<'a> {
|
||||
size: Rect,
|
||||
items: Vec<Vec<&'a str>>,
|
||||
selected: usize,
|
||||
}
|
||||
@@ -21,7 +23,6 @@ struct App<'a> {
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
size: Rect::default(),
|
||||
items: vec![
|
||||
vec!["Row12", "Row12", "Row13"],
|
||||
vec!["Row21", "Row22", "Row23"],
|
||||
@@ -35,74 +36,67 @@ impl<'a> App<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn main() -> Result<(), failure::Error> {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
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::new();
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
|
||||
// Input
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
|
||||
let evt = c.unwrap();
|
||||
match evt {
|
||||
event::Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
event::Key::Down => {
|
||||
app.selected += 1;
|
||||
if app.selected > app.items.len() - 1 {
|
||||
app.selected = 0;
|
||||
loop {
|
||||
terminal.draw(|mut f| {
|
||||
let selected_style = Style::default().fg(Color::Yellow).modifier(Modifier::BOLD);
|
||||
let normal_style = Style::default().fg(Color::White);
|
||||
let header = ["Header1", "Header2", "Header3"];
|
||||
let rows = app.items.iter().enumerate().map(|(i, item)| {
|
||||
if i == app.selected {
|
||||
Row::StyledData(item.into_iter(), selected_style)
|
||||
} else {
|
||||
Row::StyledData(item.into_iter(), normal_style)
|
||||
}
|
||||
}
|
||||
event::Key::Up => if app.selected > 0 {
|
||||
app.selected -= 1;
|
||||
} else {
|
||||
app.selected = app.items.len() - 1;
|
||||
});
|
||||
|
||||
let rects = Layout::default()
|
||||
.constraints([Constraint::Percentage(100)].as_ref())
|
||||
.margin(5)
|
||||
.split(f.size());
|
||||
Table::new(header.into_iter(), rows)
|
||||
.block(Block::default().borders(Borders::ALL).title("Table"))
|
||||
.widths(&[10, 10, 10])
|
||||
.render(&mut f, rects[0]);
|
||||
})?;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(key) => match key {
|
||||
Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
Key::Down => {
|
||||
app.selected += 1;
|
||||
if app.selected > app.items.len() - 1 {
|
||||
app.selected = 0;
|
||||
}
|
||||
}
|
||||
Key::Up => {
|
||||
if app.selected > 0 {
|
||||
app.selected -= 1;
|
||||
} else {
|
||||
app.selected = app.items.len() - 1;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
terminal.clear().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
let selected_style = Style::default().fg(Color::Yellow).modifier(Modifier::Bold);
|
||||
let normal_style = Style::default().fg(Color::White);
|
||||
let header = ["Header1", "Header2", "Header3"];
|
||||
let rows = app.items.iter().enumerate().map(|(i, item)| {
|
||||
if i == app.selected {
|
||||
Row::StyledData(item.into_iter(), &selected_style)
|
||||
} else {
|
||||
Row::StyledData(item.into_iter(), &normal_style)
|
||||
}
|
||||
});
|
||||
|
||||
let rects = Layout::default()
|
||||
.constraints([Constraint::Percentage(100)].as_ref())
|
||||
.margin(5)
|
||||
.split(app.size);
|
||||
Table::new(header.into_iter(), rows)
|
||||
.block(Block::default().borders(Borders::ALL).title("Table"))
|
||||
.widths(&[10, 10, 10])
|
||||
.render(&mut f, rects[0]);
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
143
examples/tabs.rs
143
examples/tabs.rs
@@ -1,112 +1,93 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
use util::*;
|
||||
|
||||
use std::io;
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use termion::event::Key;
|
||||
use termion::input::MouseTerminal;
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::screen::AlternateScreen;
|
||||
use tui::backend::TermionBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout};
|
||||
use tui::style::{Color, Style};
|
||||
use tui::widgets::{Block, Borders, Tabs, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use crate::util::TabsState;
|
||||
|
||||
struct App<'a> {
|
||||
size: Rect,
|
||||
tabs: MyTabs<'a>,
|
||||
tabs: TabsState<'a>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn main() -> Result<(), failure::Error> {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
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::new();
|
||||
|
||||
// App
|
||||
let mut app = App {
|
||||
size: Rect::default(),
|
||||
tabs: MyTabs {
|
||||
titles: vec!["Tab0", "Tab1", "Tab2", "Tab3"],
|
||||
selection: 0,
|
||||
},
|
||||
tabs: TabsState::new(vec!["Tab0", "Tab1", "Tab2", "Tab3"]),
|
||||
};
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &mut app).unwrap();
|
||||
|
||||
// Main loop
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
loop {
|
||||
terminal.draw(|mut f| {
|
||||
let size = f.size();
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(5)
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
||||
.split(size);
|
||||
|
||||
let evt = c.unwrap();
|
||||
match evt {
|
||||
event::Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
event::Key::Right => app.tabs.next(),
|
||||
event::Key::Left => app.tabs.previous(),
|
||||
_ => {}
|
||||
}
|
||||
draw(&mut terminal, &mut app).unwrap();
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(5)
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
||||
.split(app.size);
|
||||
|
||||
Block::default()
|
||||
.style(Style::default().bg(Color::White))
|
||||
.render(&mut f, app.size);
|
||||
Tabs::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Tabs"))
|
||||
.titles(&app.tabs.titles)
|
||||
.select(app.tabs.selection)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.highlight_style(Style::default().fg(Color::Yellow))
|
||||
.render(&mut f, chunks[0]);
|
||||
match app.tabs.selection {
|
||||
0 => {
|
||||
Block::default()
|
||||
Block::default()
|
||||
.style(Style::default().bg(Color::White))
|
||||
.render(&mut f, size);
|
||||
Tabs::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Tabs"))
|
||||
.titles(&app.tabs.titles)
|
||||
.select(app.tabs.index)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.highlight_style(Style::default().fg(Color::Yellow))
|
||||
.render(&mut f, chunks[0]);
|
||||
match app.tabs.index {
|
||||
0 => Block::default()
|
||||
.title("Inner 0")
|
||||
.borders(Borders::ALL)
|
||||
.render(&mut f, chunks[1]);
|
||||
}
|
||||
1 => {
|
||||
Block::default()
|
||||
.render(&mut f, chunks[1]),
|
||||
1 => Block::default()
|
||||
.title("Inner 1")
|
||||
.borders(Borders::ALL)
|
||||
.render(&mut f, chunks[1]);
|
||||
}
|
||||
2 => {
|
||||
Block::default()
|
||||
.render(&mut f, chunks[1]),
|
||||
2 => Block::default()
|
||||
.title("Inner 2")
|
||||
.borders(Borders::ALL)
|
||||
.render(&mut f, chunks[1]);
|
||||
}
|
||||
3 => {
|
||||
Block::default()
|
||||
.render(&mut f, chunks[1]),
|
||||
3 => Block::default()
|
||||
.title("Inner 3")
|
||||
.borders(Borders::ALL)
|
||||
.render(&mut f, chunks[1]);
|
||||
.render(&mut f, chunks[1]),
|
||||
_ => {}
|
||||
}
|
||||
})?;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(input) => match input {
|
||||
Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
Key::Right => app.tabs.next(),
|
||||
Key::Left => app.tabs.previous(),
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
75
examples/termion_demo.rs
Normal file
75
examples/termion_demo.rs
Normal 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(())
|
||||
}
|
||||
@@ -9,121 +9,105 @@
|
||||
/// * Pressing Backspace erases a character
|
||||
/// * Pressing Enter pushes the current input in the history of previous
|
||||
/// messages
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
use std::io::{self, Write};
|
||||
|
||||
use tui::backend::AlternateScreenBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use termion::cursor::Goto;
|
||||
use termion::event::Key;
|
||||
use termion::input::MouseTerminal;
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::screen::AlternateScreen;
|
||||
use tui::backend::TermionBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout};
|
||||
use tui::style::{Color, Style};
|
||||
use tui::widgets::{Block, Borders, Item, List, Paragraph, Text, Widget};
|
||||
use tui::widgets::{Block, Borders, List, Paragraph, Text, Widget};
|
||||
use tui::Terminal;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
|
||||
/// App holds the state of the application
|
||||
struct App {
|
||||
size: Rect,
|
||||
/// Current value of the input box
|
||||
input: String,
|
||||
/// History of recorded messages
|
||||
messages: Vec<String>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
impl Default for App {
|
||||
fn default() -> App {
|
||||
App {
|
||||
size: Rect::default(),
|
||||
input: String::new(),
|
||||
messages: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
}
|
||||
|
||||
fn main() {
|
||||
fn main() -> Result<(), failure::Error> {
|
||||
// Terminal initialization
|
||||
let backend = AlternateScreenBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
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)?;
|
||||
|
||||
// Channels
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
// Setup event handlers
|
||||
let events = Events::new();
|
||||
|
||||
// Input
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
// Create default app state
|
||||
let mut app = App::default();
|
||||
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if app.size != size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
// Draw UI
|
||||
terminal.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints([Constraint::Length(3), Constraint::Min(1)].as_ref())
|
||||
.split(f.size());
|
||||
Paragraph::new([Text::raw(&app.input)].iter())
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.block(Block::default().borders(Borders::ALL).title("Input"))
|
||||
.render(&mut f, chunks[0]);
|
||||
let messages = app
|
||||
.messages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, m)| Text::raw(format!("{}: {}", i, m)));
|
||||
List::new(messages)
|
||||
.block(Block::default().borders(Borders::ALL).title("Messages"))
|
||||
.render(&mut f, chunks[1]);
|
||||
})?;
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
// Put the cursor back inside the input box
|
||||
write!(
|
||||
terminal.backend_mut(),
|
||||
"{}",
|
||||
Goto(4 + app.input.width() as u16, 4)
|
||||
)?;
|
||||
|
||||
// Handle input
|
||||
match events.next()? {
|
||||
Event::Input(input) => match input {
|
||||
event::Key::Char('q') => {
|
||||
Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
event::Key::Char('\n') => {
|
||||
Key::Char('\n') => {
|
||||
app.messages.push(app.input.drain(..).collect());
|
||||
}
|
||||
event::Key::Char(c) => {
|
||||
Key::Char(c) => {
|
||||
app.input.push(c);
|
||||
}
|
||||
event::Key::Backspace => {
|
||||
Key::Backspace => {
|
||||
app.input.pop();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
terminal.clear().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<AlternateScreenBackend>, app: &App) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints([Constraint::Length(3), Constraint::Min(1)].as_ref())
|
||||
.split(app.size);
|
||||
Paragraph::new([Text::Data(&app.input)].iter())
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.block(Block::default().borders(Borders::ALL).title("Input"))
|
||||
.render(&mut f, chunks[0]);
|
||||
List::new(
|
||||
app.messages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, m)| Item::Data(format!("{}: {}", i, m))),
|
||||
).block(Block::default().borders(Borders::ALL).title("Messages"))
|
||||
.render(&mut f, chunks[1]);
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
83
examples/util/event.rs
Normal file
83
examples/util/event.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use termion::event::Key;
|
||||
use termion::input::TermRead;
|
||||
|
||||
pub enum Event<I> {
|
||||
Input(I),
|
||||
Tick,
|
||||
}
|
||||
|
||||
/// A small event handler that wrap termion input and tick events. Each event
|
||||
/// type is handled in its own thread and returned to a common `Receiver`
|
||||
pub struct Events {
|
||||
rx: mpsc::Receiver<Event<Key>>,
|
||||
input_handle: thread::JoinHandle<()>,
|
||||
tick_handle: thread::JoinHandle<()>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Config {
|
||||
pub exit_key: Key,
|
||||
pub tick_rate: Duration,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Config {
|
||||
Config {
|
||||
exit_key: Key::Char('q'),
|
||||
tick_rate: Duration::from_millis(250),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Events {
|
||||
pub fn new() -> Events {
|
||||
Events::with_config(Config::default())
|
||||
}
|
||||
|
||||
pub fn with_config(config: Config) -> Events {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_handle = {
|
||||
let tx = tx.clone();
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for evt in stdin.keys() {
|
||||
match evt {
|
||||
Ok(key) => {
|
||||
if let Err(_) = tx.send(Event::Input(key)) {
|
||||
return;
|
||||
}
|
||||
if key == config.exit_key {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
let tick_handle = {
|
||||
let tx = tx.clone();
|
||||
thread::spawn(move || {
|
||||
let tx = tx.clone();
|
||||
loop {
|
||||
tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(config.tick_rate);
|
||||
}
|
||||
})
|
||||
};
|
||||
Events {
|
||||
rx,
|
||||
input_handle,
|
||||
tick_handle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&self) -> Result<Event<Key>, mpsc::RecvError> {
|
||||
self.rx.recv()
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
#![allow(dead_code)]
|
||||
#[cfg(feature = "termion")]
|
||||
pub mod event;
|
||||
|
||||
extern crate rand;
|
||||
|
||||
use self::rand::distributions::{IndependentSample, Range};
|
||||
use rand::distributions::{Distribution, Uniform};
|
||||
use rand::rngs::ThreadRng;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RandomSignal {
|
||||
range: Range<u64>,
|
||||
rng: rand::ThreadRng,
|
||||
distribution: Uniform<u64>,
|
||||
rng: ThreadRng,
|
||||
}
|
||||
|
||||
impl RandomSignal {
|
||||
pub fn new(lower: u64, upper: u64) -> RandomSignal {
|
||||
RandomSignal {
|
||||
range: Range::new(lower, upper),
|
||||
distribution: Uniform::new(lower, upper),
|
||||
rng: rand::thread_rng(),
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ impl RandomSignal {
|
||||
impl Iterator for RandomSignal {
|
||||
type Item = u64;
|
||||
fn next(&mut self) -> Option<u64> {
|
||||
Some(self.range.ind_sample(&mut self.rng))
|
||||
Some(self.distribution.sample(&mut self.rng))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,21 +54,24 @@ impl Iterator for SinSignal {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MyTabs<'a> {
|
||||
pub struct TabsState<'a> {
|
||||
pub titles: Vec<&'a str>,
|
||||
pub selection: usize,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
impl<'a> MyTabs<'a> {
|
||||
impl<'a> TabsState<'a> {
|
||||
pub fn new(titles: Vec<&'a str>) -> TabsState {
|
||||
TabsState { titles, index: 0 }
|
||||
}
|
||||
pub fn next(&mut self) {
|
||||
self.selection = (self.selection + 1) % self.titles.len();
|
||||
self.index = (self.index + 1) % self.titles.len();
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
if self.selection > 0 {
|
||||
self.selection -= 1;
|
||||
if self.index > 0 {
|
||||
self.index -= 1;
|
||||
} else {
|
||||
self.selection = self.titles.len() - 1;
|
||||
self.index = self.titles.len() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build all examples in examples directory
|
||||
|
||||
set -u -o pipefail
|
||||
|
||||
for file in examples/*.rs; do
|
||||
name=$(basename ${file//.rs/})
|
||||
echo "[EXAMPLE] $name"
|
||||
if [[ "$name" == "rustbox" ]]; then
|
||||
cargo build --features rustbox --example "$name"
|
||||
else
|
||||
cargo build --example "$name"
|
||||
fi
|
||||
done
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run all examples in examples directory
|
||||
|
||||
set -u -o pipefail
|
||||
|
||||
for file in examples/*.rs; do
|
||||
name=$(basename ${file//.rs/})
|
||||
if [[ "$name" == "rustbox" ]]; then
|
||||
cargo run --features rustbox --example "$name"
|
||||
else
|
||||
cargo run --example "$name"
|
||||
fi
|
||||
done
|
||||
@@ -1,56 +1,126 @@
|
||||
extern crate crossterm;
|
||||
|
||||
use std::io;
|
||||
|
||||
use backend::Backend;
|
||||
use buffer::Cell;
|
||||
use layout::Rect;
|
||||
use style::{Color, Modifier};
|
||||
use crate::backend::Backend;
|
||||
use crate::buffer::Cell;
|
||||
use crate::layout::Rect;
|
||||
use crate::style::{Color, Modifier};
|
||||
use crossterm::error::ErrorKind;
|
||||
|
||||
pub struct CrosstermBackend {
|
||||
screen: crossterm::Screen,
|
||||
screen: Option<crossterm::Screen>,
|
||||
crossterm: crossterm::Crossterm,
|
||||
// Need to keep the AlternateScreen around even when not using it directly,
|
||||
// see https://github.com/TimonPost/crossterm/issues/88
|
||||
alternate_screen: Option<crossterm::AlternateScreen>,
|
||||
}
|
||||
|
||||
impl Default for CrosstermBackend {
|
||||
fn default() -> CrosstermBackend {
|
||||
let screen = crossterm::Screen::default();
|
||||
let crossterm = crossterm::Crossterm::from_screen(&screen);
|
||||
CrosstermBackend {
|
||||
screen: Some(screen),
|
||||
crossterm,
|
||||
alternate_screen: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CrosstermBackend {
|
||||
pub fn new() -> CrosstermBackend {
|
||||
CrosstermBackend::default()
|
||||
}
|
||||
|
||||
pub fn with_screen(screen: crossterm::Screen) -> CrosstermBackend {
|
||||
let crossterm = crossterm::Crossterm::from_screen(&screen);
|
||||
CrosstermBackend {
|
||||
screen: crossterm::Screen::default(),
|
||||
screen: Some(screen),
|
||||
crossterm,
|
||||
alternate_screen: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn screen(&self) -> &crossterm::Screen {
|
||||
&self.screen
|
||||
pub fn with_alternate_screen(
|
||||
alternate_screen: crossterm::AlternateScreen,
|
||||
) -> Result<CrosstermBackend, io::Error> {
|
||||
let crossterm = crossterm::Crossterm::from_screen(&alternate_screen.screen);
|
||||
Ok(CrosstermBackend {
|
||||
screen: None,
|
||||
crossterm,
|
||||
alternate_screen: Some(alternate_screen),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn screen(&self) -> Option<&crossterm::Screen> {
|
||||
match &self.screen {
|
||||
Some(screen) => Some(&screen),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn alternate_screen(&self) -> Option<&crossterm::AlternateScreen> {
|
||||
match &self.alternate_screen {
|
||||
Some(alt_screen) => Some(&alt_screen),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn crossterm(&self) -> &crossterm::Crossterm {
|
||||
&self.crossterm
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: consider associated Error type on Backend to allow custom error types
|
||||
// per backend
|
||||
fn convert_error(error: ErrorKind) -> io::Error {
|
||||
match error {
|
||||
ErrorKind::IoError(err) => err,
|
||||
ErrorKind::FmtError(err) => {
|
||||
io::Error::new(io::ErrorKind::Other, format!("Invalid formatting: {}", err))
|
||||
}
|
||||
ErrorKind::ResizingTerminalFailure(err) => io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Failed to resize terminal: {}", err),
|
||||
),
|
||||
_ => io::Error::new(io::ErrorKind::Other, "Unknown crossterm error"),
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for CrosstermBackend {
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
let terminal = crossterm::terminal::terminal(&self.screen);
|
||||
terminal.clear(crossterm::terminal::ClearType::All);
|
||||
let terminal = self.crossterm.terminal();
|
||||
terminal
|
||||
.clear(crossterm::ClearType::All)
|
||||
.map_err(convert_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
let cursor = crossterm::cursor(&self.screen);
|
||||
cursor.hide();
|
||||
let cursor = self.crossterm.cursor();
|
||||
cursor.hide().map_err(convert_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
let cursor = crossterm::cursor(&self.screen);
|
||||
cursor.show();
|
||||
let cursor = self.crossterm.cursor();
|
||||
cursor.show().map_err(convert_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
let cursor = crossterm::cursor();
|
||||
Ok(cursor.pos())
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
let cursor = crossterm::cursor();
|
||||
cursor.goto(x, y).map_err(convert_error)
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
let terminal = crossterm::terminal::terminal(&self.screen);
|
||||
let terminal = self.crossterm.terminal();
|
||||
let (width, height) = terminal.terminal_size();
|
||||
Ok(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
Ok(Rect::new(0, 0, width, height))
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
@@ -61,27 +131,27 @@ impl Backend for CrosstermBackend {
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
let cursor = crossterm::cursor(&self.screen);
|
||||
let cursor = self.crossterm.cursor();
|
||||
let mut last_y = 0;
|
||||
let mut last_x = 0;
|
||||
let mut first = true;
|
||||
for (x, y, cell) in content {
|
||||
if y != last_y || x != last_x + 1 {
|
||||
cursor.goto(x + 1, y + 1);
|
||||
if y != last_y || x != last_x + 1 || first {
|
||||
cursor.goto(x, y).map_err(convert_error)?;
|
||||
first = false;
|
||||
}
|
||||
last_x = x;
|
||||
last_y = y;
|
||||
|
||||
let mut s = crossterm::style(&cell.symbol);
|
||||
let mut s = self.crossterm.style(&cell.symbol);
|
||||
if let Some(color) = cell.style.fg.into() {
|
||||
s = s.with(color)
|
||||
}
|
||||
if let Some(color) = cell.style.bg.into() {
|
||||
s = s.on(color)
|
||||
}
|
||||
if let Some(attr) = cell.style.modifier.into() {
|
||||
s = s.attr(attr)
|
||||
}
|
||||
s.paint(&self.screen);
|
||||
s.object_style.attrs = cell.style.modifier.into();
|
||||
|
||||
self.crossterm.paint(s).map_err(convert_error)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -107,22 +177,59 @@ impl From<Color> for Option<crossterm::Color> {
|
||||
Color::LightMagenta => Some(crossterm::Color::Magenta),
|
||||
Color::LightCyan => Some(crossterm::Color::Cyan),
|
||||
Color::White => Some(crossterm::Color::White),
|
||||
Color::Indexed(i) => Some(crossterm::Color::AnsiValue(i)),
|
||||
Color::Rgb(r, g, b) => Some(crossterm::Color::Rgb { r, g, b }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Modifier> for Option<crossterm::Attribute> {
|
||||
fn from(modifier: Modifier) -> Option<crossterm::Attribute> {
|
||||
match modifier {
|
||||
Modifier::Blink => Some(crossterm::Attribute::SlowBlink),
|
||||
Modifier::Bold => Some(crossterm::Attribute::Bold),
|
||||
Modifier::CrossedOut => Some(crossterm::Attribute::CrossedOut),
|
||||
Modifier::Faint => Some(crossterm::Attribute::Dim),
|
||||
Modifier::Invert => Some(crossterm::Attribute::Reverse),
|
||||
Modifier::Italic => Some(crossterm::Attribute::Italic),
|
||||
Modifier::Underline => Some(crossterm::Attribute::Underlined),
|
||||
_ => None,
|
||||
impl From<Modifier> for Vec<crossterm::Attribute> {
|
||||
#[cfg(unix)]
|
||||
fn from(modifier: Modifier) -> Vec<crossterm::Attribute> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
if modifier.contains(Modifier::BOLD) {
|
||||
result.push(crossterm::Attribute::Bold)
|
||||
}
|
||||
if modifier.contains(Modifier::DIM) {
|
||||
result.push(crossterm::Attribute::Dim)
|
||||
}
|
||||
if modifier.contains(Modifier::ITALIC) {
|
||||
result.push(crossterm::Attribute::Italic)
|
||||
}
|
||||
if modifier.contains(Modifier::UNDERLINED) {
|
||||
result.push(crossterm::Attribute::Underlined)
|
||||
}
|
||||
if modifier.contains(Modifier::SLOW_BLINK) {
|
||||
result.push(crossterm::Attribute::SlowBlink)
|
||||
}
|
||||
if modifier.contains(Modifier::RAPID_BLINK) {
|
||||
result.push(crossterm::Attribute::RapidBlink)
|
||||
}
|
||||
if modifier.contains(Modifier::REVERSED) {
|
||||
result.push(crossterm::Attribute::Reverse)
|
||||
}
|
||||
if modifier.contains(Modifier::HIDDEN) {
|
||||
result.push(crossterm::Attribute::Hidden)
|
||||
}
|
||||
if modifier.contains(Modifier::CROSSED_OUT) {
|
||||
result.push(crossterm::Attribute::CrossedOut)
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn from(modifier: Modifier) -> Vec<crossterm::Attribute> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
if modifier.contains(Modifier::BOLD) {
|
||||
result.push(crossterm::Attribute::Bold)
|
||||
}
|
||||
if modifier.contains(Modifier::UNDERLINED) {
|
||||
result.push(crossterm::Attribute::Underlined)
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
283
src/backend/curses.rs
Normal file
283
src/backend/curses.rs
Normal file
@@ -0,0 +1,283 @@
|
||||
use std::io;
|
||||
|
||||
use crate::backend::Backend;
|
||||
use crate::buffer::Cell;
|
||||
use crate::layout::Rect;
|
||||
use crate::style::{Color, Modifier, Style};
|
||||
use crate::symbols::{bar, block};
|
||||
#[cfg(unix)]
|
||||
use crate::symbols::{line, DOT};
|
||||
#[cfg(unix)]
|
||||
use pancurses::{chtype, ToChtype};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
pub struct CursesBackend {
|
||||
curses: easycurses::EasyCurses,
|
||||
}
|
||||
|
||||
impl CursesBackend {
|
||||
pub fn new() -> Option<CursesBackend> {
|
||||
let curses = easycurses::EasyCurses::initialize_system()?;
|
||||
Some(CursesBackend { curses })
|
||||
}
|
||||
|
||||
pub fn with_curses(curses: easycurses::EasyCurses) -> CursesBackend {
|
||||
CursesBackend { curses }
|
||||
}
|
||||
|
||||
pub fn get_curses(&self) -> &easycurses::EasyCurses {
|
||||
&self.curses
|
||||
}
|
||||
|
||||
pub fn get_curses_mut(&mut self) -> &mut easycurses::EasyCurses {
|
||||
&mut self.curses
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for CursesBackend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
let mut last_col = 0;
|
||||
let mut last_row = 0;
|
||||
let mut style = Style {
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
modifier: Modifier::empty(),
|
||||
};
|
||||
let mut curses_style = CursesStyle {
|
||||
fg: easycurses::Color::White,
|
||||
bg: easycurses::Color::Black,
|
||||
};
|
||||
let mut update_color = false;
|
||||
for (col, row, cell) in content {
|
||||
// eprintln!("{:?}", cell);
|
||||
if row != last_row || col != last_col + 1 {
|
||||
self.curses.move_rc(row as i32, col as i32);
|
||||
}
|
||||
last_col = col;
|
||||
last_row = row;
|
||||
if cell.style.modifier != style.modifier {
|
||||
apply_modifier_diff(&mut self.curses.win, style.modifier, cell.style.modifier);
|
||||
style.modifier = cell.style.modifier;
|
||||
};
|
||||
if cell.style.fg != style.fg {
|
||||
update_color = true;
|
||||
if let Some(ccolor) = cell.style.fg.into() {
|
||||
style.fg = cell.style.fg;
|
||||
curses_style.fg = ccolor;
|
||||
} else {
|
||||
style.fg = Color::White;
|
||||
curses_style.fg = easycurses::Color::White;
|
||||
}
|
||||
};
|
||||
if cell.style.bg != style.bg {
|
||||
update_color = true;
|
||||
if let Some(ccolor) = cell.style.bg.into() {
|
||||
style.bg = cell.style.bg;
|
||||
curses_style.bg = ccolor;
|
||||
} else {
|
||||
style.bg = Color::Black;
|
||||
curses_style.bg = easycurses::Color::Black;
|
||||
}
|
||||
};
|
||||
if update_color {
|
||||
self.curses
|
||||
.set_color_pair(easycurses::ColorPair::new(curses_style.fg, curses_style.bg));
|
||||
};
|
||||
update_color = false;
|
||||
draw(&mut self.curses, cell.symbol.as_str());
|
||||
}
|
||||
self.curses.win.attrset(pancurses::Attribute::Normal);
|
||||
self.curses.set_color_pair(easycurses::ColorPair::new(
|
||||
easycurses::Color::White,
|
||||
easycurses::Color::Black,
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.curses
|
||||
.set_cursor_visibility(easycurses::CursorVisibility::Invisible);
|
||||
Ok(())
|
||||
}
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.curses
|
||||
.set_cursor_visibility(easycurses::CursorVisibility::Visible);
|
||||
Ok(())
|
||||
}
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
let (x, y) = self.curses.get_cursor_rc();
|
||||
Ok((x as u16, y as u16))
|
||||
}
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.curses.move_rc(x as i32, y as i32);
|
||||
Ok(())
|
||||
}
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.curses.clear();
|
||||
// self.curses.refresh();
|
||||
Ok(())
|
||||
}
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
let (nrows, ncols) = self.curses.get_row_col_count();
|
||||
Ok(Rect::new(0, 0, ncols as u16, nrows as u16))
|
||||
}
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.curses.refresh();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct CursesStyle {
|
||||
fg: easycurses::Color,
|
||||
bg: easycurses::Color,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
/// Deals with lack of unicode support for ncurses on unix
|
||||
fn draw(curses: &mut easycurses::EasyCurses, symbol: &str) {
|
||||
for grapheme in symbol.graphemes(true) {
|
||||
let ch = match grapheme {
|
||||
line::TOP_RIGHT => pancurses::ACS_URCORNER(),
|
||||
line::VERTICAL => pancurses::ACS_VLINE(),
|
||||
line::HORIZONTAL => pancurses::ACS_HLINE(),
|
||||
line::TOP_LEFT => pancurses::ACS_ULCORNER(),
|
||||
line::BOTTOM_RIGHT => pancurses::ACS_LRCORNER(),
|
||||
line::BOTTOM_LEFT => pancurses::ACS_LLCORNER(),
|
||||
line::VERTICAL_LEFT => pancurses::ACS_RTEE(),
|
||||
line::VERTICAL_RIGHT => pancurses::ACS_LTEE(),
|
||||
line::HORIZONTAL_DOWN => pancurses::ACS_TTEE(),
|
||||
line::HORIZONTAL_UP => pancurses::ACS_BTEE(),
|
||||
block::FULL => pancurses::ACS_BLOCK(),
|
||||
block::SEVEN_EIGHTHS => pancurses::ACS_BLOCK(),
|
||||
block::THREE_QUATERS => pancurses::ACS_BLOCK(),
|
||||
block::FIVE_EIGHTHS => pancurses::ACS_BLOCK(),
|
||||
block::HALF => pancurses::ACS_BLOCK(),
|
||||
block::THREE_EIGHTHS => ' ' as chtype,
|
||||
block::ONE_QUATER => ' ' as chtype,
|
||||
block::ONE_EIGHTH => ' ' as chtype,
|
||||
bar::SEVEN_EIGHTHS => pancurses::ACS_BLOCK(),
|
||||
bar::THREE_QUATERS => pancurses::ACS_BLOCK(),
|
||||
bar::FIVE_EIGHTHS => pancurses::ACS_BLOCK(),
|
||||
bar::HALF => pancurses::ACS_BLOCK(),
|
||||
bar::THREE_EIGHTHS => pancurses::ACS_S9(),
|
||||
bar::ONE_QUATER => pancurses::ACS_S9(),
|
||||
bar::ONE_EIGHTH => pancurses::ACS_S9(),
|
||||
DOT => pancurses::ACS_BULLET(),
|
||||
unicode_char => {
|
||||
if unicode_char.is_ascii() {
|
||||
let mut chars = unicode_char.chars();
|
||||
if let Some(ch) = chars.next() {
|
||||
ch.to_chtype()
|
||||
} else {
|
||||
pancurses::ACS_BLOCK()
|
||||
}
|
||||
} else {
|
||||
pancurses::ACS_BLOCK()
|
||||
}
|
||||
}
|
||||
};
|
||||
curses.win.addch(ch);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn draw(curses: &mut easycurses::EasyCurses, symbol: &str) {
|
||||
for grapheme in symbol.graphemes(true) {
|
||||
let ch = match grapheme {
|
||||
block::SEVEN_EIGHTHS => block::FULL,
|
||||
block::THREE_QUATERS => block::FULL,
|
||||
block::FIVE_EIGHTHS => block::HALF,
|
||||
block::THREE_EIGHTHS => block::HALF,
|
||||
block::ONE_QUATER => block::HALF,
|
||||
block::ONE_EIGHTH => " ",
|
||||
bar::SEVEN_EIGHTHS => bar::FULL,
|
||||
bar::THREE_QUATERS => bar::FULL,
|
||||
bar::FIVE_EIGHTHS => bar::HALF,
|
||||
bar::THREE_EIGHTHS => bar::HALF,
|
||||
bar::ONE_QUATER => bar::HALF,
|
||||
bar::ONE_EIGHTH => " ",
|
||||
ch => ch,
|
||||
};
|
||||
// curses.win.addch(ch);
|
||||
curses.print(ch);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for Option<easycurses::Color> {
|
||||
fn from(color: Color) -> Option<easycurses::Color> {
|
||||
match color {
|
||||
Color::Reset => None,
|
||||
Color::Black => Some(easycurses::Color::Black),
|
||||
Color::Red | Color::LightRed => Some(easycurses::Color::Red),
|
||||
Color::Green | Color::LightGreen => Some(easycurses::Color::Green),
|
||||
Color::Yellow | Color::LightYellow => Some(easycurses::Color::Yellow),
|
||||
Color::Magenta | Color::LightMagenta => Some(easycurses::Color::Magenta),
|
||||
Color::Cyan | Color::LightCyan => Some(easycurses::Color::Cyan),
|
||||
Color::White | Color::Gray | Color::DarkGray => Some(easycurses::Color::White),
|
||||
Color::Blue | Color::LightBlue => Some(easycurses::Color::Blue),
|
||||
Color::Indexed(_) => None,
|
||||
Color::Rgb(_, _, _) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_modifier_diff(win: &mut pancurses::Window, from: Modifier, to: Modifier) {
|
||||
remove_modifier(win, from - to);
|
||||
add_modifier(win, to - from);
|
||||
}
|
||||
|
||||
fn remove_modifier(win: &mut pancurses::Window, remove: Modifier) {
|
||||
if remove.contains(Modifier::BOLD) {
|
||||
win.attroff(pancurses::Attribute::Bold);
|
||||
}
|
||||
if remove.contains(Modifier::DIM) {
|
||||
win.attroff(pancurses::Attribute::Dim);
|
||||
}
|
||||
if remove.contains(Modifier::ITALIC) {
|
||||
win.attroff(pancurses::Attribute::Italic);
|
||||
}
|
||||
if remove.contains(Modifier::UNDERLINED) {
|
||||
win.attroff(pancurses::Attribute::Underline);
|
||||
}
|
||||
if remove.contains(Modifier::SLOW_BLINK) || remove.contains(Modifier::RAPID_BLINK) {
|
||||
win.attroff(pancurses::Attribute::Blink);
|
||||
}
|
||||
if remove.contains(Modifier::REVERSED) {
|
||||
win.attroff(pancurses::Attribute::Reverse);
|
||||
}
|
||||
if remove.contains(Modifier::HIDDEN) {
|
||||
win.attroff(pancurses::Attribute::Invisible);
|
||||
}
|
||||
if remove.contains(Modifier::CROSSED_OUT) {
|
||||
win.attroff(pancurses::Attribute::Strikeout);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_modifier(win: &mut pancurses::Window, add: Modifier) {
|
||||
if add.contains(Modifier::BOLD) {
|
||||
win.attron(pancurses::Attribute::Bold);
|
||||
}
|
||||
if add.contains(Modifier::DIM) {
|
||||
win.attron(pancurses::Attribute::Dim);
|
||||
}
|
||||
if add.contains(Modifier::ITALIC) {
|
||||
win.attron(pancurses::Attribute::Italic);
|
||||
}
|
||||
if add.contains(Modifier::UNDERLINED) {
|
||||
win.attron(pancurses::Attribute::Underline);
|
||||
}
|
||||
if add.contains(Modifier::SLOW_BLINK) || add.contains(Modifier::RAPID_BLINK) {
|
||||
win.attron(pancurses::Attribute::Blink);
|
||||
}
|
||||
if add.contains(Modifier::REVERSED) {
|
||||
win.attron(pancurses::Attribute::Reverse);
|
||||
}
|
||||
if add.contains(Modifier::HIDDEN) {
|
||||
win.attron(pancurses::Attribute::Invisible);
|
||||
}
|
||||
if add.contains(Modifier::CROSSED_OUT) {
|
||||
win.attron(pancurses::Attribute::Strikeout);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::io;
|
||||
|
||||
use buffer::Cell;
|
||||
use layout::Rect;
|
||||
use crate::buffer::Cell;
|
||||
use crate::layout::Rect;
|
||||
|
||||
#[cfg(feature = "rustbox")]
|
||||
mod rustbox;
|
||||
@@ -11,19 +11,29 @@ pub use self::rustbox::RustboxBackend;
|
||||
#[cfg(feature = "termion")]
|
||||
mod termion;
|
||||
#[cfg(feature = "termion")]
|
||||
pub use self::termion::{AlternateScreenBackend, MouseBackend, RawBackend, TermionBackend};
|
||||
pub use self::termion::TermionBackend;
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
mod crossterm;
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub use self::crossterm::CrosstermBackend;
|
||||
|
||||
#[cfg(feature = "curses")]
|
||||
mod curses;
|
||||
#[cfg(feature = "curses")]
|
||||
pub use self::curses::CursesBackend;
|
||||
|
||||
mod test;
|
||||
pub use self::test::TestBackend;
|
||||
|
||||
pub trait Backend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>;
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error>;
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error>;
|
||||
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error>;
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error>;
|
||||
fn clear(&mut self) -> Result<(), io::Error>;
|
||||
fn size(&self) -> Result<Rect, io::Error>;
|
||||
fn flush(&mut self) -> Result<(), io::Error>;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
extern crate rustbox;
|
||||
|
||||
use log::debug;
|
||||
use std::io;
|
||||
|
||||
use super::Backend;
|
||||
use buffer::Cell;
|
||||
use layout::Rect;
|
||||
use style::{Color, Modifier};
|
||||
use crate::buffer::Cell;
|
||||
use crate::layout::Rect;
|
||||
use crate::style::{Color, Modifier};
|
||||
|
||||
pub struct RustboxBackend {
|
||||
rustbox: rustbox::RustBox,
|
||||
@@ -13,8 +12,8 @@ pub struct RustboxBackend {
|
||||
|
||||
impl RustboxBackend {
|
||||
pub fn new() -> Result<RustboxBackend, rustbox::InitError> {
|
||||
let rustbox = try!(rustbox::RustBox::init(Default::default()));
|
||||
Ok(RustboxBackend { rustbox: rustbox })
|
||||
let rustbox = r#try!(rustbox::RustBox::init(Default::default()));
|
||||
Ok(RustboxBackend { rustbox })
|
||||
}
|
||||
|
||||
pub fn with_rustbox(instance: rustbox::RustBox) -> RustboxBackend {
|
||||
@@ -52,17 +51,35 @@ impl Backend for RustboxBackend {
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
Err(io::Error::from(io::ErrorKind::Other))
|
||||
}
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.rustbox.set_cursor(x as isize, y as isize);
|
||||
Ok(())
|
||||
}
|
||||
fn clear(&mut self) -> Result<(), io::Error> {
|
||||
self.rustbox.clear();
|
||||
Ok(())
|
||||
}
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
Ok(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: self.rustbox.width() as u16,
|
||||
height: self.rustbox.height() as u16,
|
||||
})
|
||||
let term_width = self.rustbox.width();
|
||||
let term_height = self.rustbox.height();
|
||||
let max = u16::max_value();
|
||||
Ok(Rect::new(
|
||||
0,
|
||||
0,
|
||||
if term_width > usize::from(max) {
|
||||
max
|
||||
} else {
|
||||
term_width as u16
|
||||
},
|
||||
if term_height > usize::from(max) {
|
||||
max
|
||||
} else {
|
||||
term_height as u16
|
||||
},
|
||||
))
|
||||
}
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
self.rustbox.present();
|
||||
@@ -86,6 +103,7 @@ impl Into<rustbox::Color> for Color {
|
||||
Color::Cyan | Color::LightCyan => rustbox::Color::Cyan,
|
||||
Color::White => rustbox::Color::White,
|
||||
Color::Blue | Color::LightBlue => rustbox::Color::Blue,
|
||||
Color::Indexed(i) => rustbox::Color::Byte(i as u16),
|
||||
Color::Rgb(r, g, b) => rustbox::Color::Byte(rgb_to_byte(r, g, b)),
|
||||
}
|
||||
}
|
||||
@@ -93,11 +111,16 @@ impl Into<rustbox::Color> for Color {
|
||||
|
||||
impl Into<rustbox::Style> for Modifier {
|
||||
fn into(self) -> rustbox::Style {
|
||||
match self {
|
||||
Modifier::Bold => rustbox::RB_BOLD,
|
||||
Modifier::Underline => rustbox::RB_UNDERLINE,
|
||||
Modifier::Invert => rustbox::RB_REVERSE,
|
||||
_ => rustbox::RB_NORMAL,
|
||||
let mut result = rustbox::Style::empty();
|
||||
if self.contains(Modifier::BOLD) {
|
||||
result.insert(rustbox::RB_BOLD);
|
||||
}
|
||||
if self.contains(Modifier::UNDERLINED) {
|
||||
result.insert(rustbox::RB_UNDERLINE);
|
||||
}
|
||||
if self.contains(Modifier::REVERSED) {
|
||||
result.insert(rustbox::RB_REVERSE);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
extern crate termion;
|
||||
|
||||
use log::debug;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
|
||||
use self::termion::raw::IntoRawMode;
|
||||
|
||||
use super::Backend;
|
||||
use buffer::Cell;
|
||||
use layout::Rect;
|
||||
use style::{Color, Modifier, Style};
|
||||
use crate::buffer::Cell;
|
||||
use crate::layout::Rect;
|
||||
use crate::style;
|
||||
|
||||
pub struct TermionBackend<W>
|
||||
where
|
||||
@@ -17,40 +15,11 @@ where
|
||||
stdout: W,
|
||||
}
|
||||
|
||||
pub type RawBackend = TermionBackend<termion::raw::RawTerminal<io::Stdout>>;
|
||||
pub type MouseBackend =
|
||||
TermionBackend<termion::input::MouseTerminal<termion::raw::RawTerminal<io::Stdout>>>;
|
||||
pub type AlternateScreenBackend =
|
||||
TermionBackend<termion::screen::AlternateScreen<termion::raw::RawTerminal<io::Stdout>>>;
|
||||
|
||||
impl RawBackend {
|
||||
pub fn new() -> Result<RawBackend, io::Error> {
|
||||
let raw = io::stdout().into_raw_mode()?;
|
||||
Ok(TermionBackend::with_stdout(raw))
|
||||
}
|
||||
}
|
||||
|
||||
impl MouseBackend {
|
||||
pub fn new() -> Result<MouseBackend, io::Error> {
|
||||
let raw = io::stdout().into_raw_mode()?;
|
||||
let mouse = termion::input::MouseTerminal::from(raw);
|
||||
Ok(TermionBackend::with_stdout(mouse))
|
||||
}
|
||||
}
|
||||
|
||||
impl AlternateScreenBackend {
|
||||
pub fn new() -> Result<AlternateScreenBackend, io::Error> {
|
||||
let raw = io::stdout().into_raw_mode()?;
|
||||
let screen = termion::screen::AlternateScreen::from(raw);
|
||||
Ok(TermionBackend::with_stdout(screen))
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> TermionBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
pub fn with_stdout(stdout: W) -> TermionBackend<W> {
|
||||
pub fn new(stdout: W) -> TermionBackend<W> {
|
||||
TermionBackend { stdout }
|
||||
}
|
||||
}
|
||||
@@ -91,38 +60,55 @@ where
|
||||
self.stdout.flush()
|
||||
}
|
||||
|
||||
/// Gets cursor position (0-based index)
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
termion::cursor::DetectCursorPos::cursor_pos(&mut self.stdout).map(|(x, y)| (x - 1, y - 1))
|
||||
}
|
||||
|
||||
/// Sets cursor position (0-based index)
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
write!(self.stdout, "{}", termion::cursor::Goto(x + 1, y + 1))?;
|
||||
self.stdout.flush()
|
||||
}
|
||||
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut string = String::with_capacity(content.size_hint().0 * 3);
|
||||
let mut style = Style::default();
|
||||
let mut style = style::Style::default();
|
||||
let mut last_y = 0;
|
||||
let mut last_x = 0;
|
||||
let mut inst = 0;
|
||||
for (x, y, cell) in content {
|
||||
if y != last_y || x != last_x + 1 {
|
||||
string.push_str(&format!("{}", termion::cursor::Goto(x + 1, y + 1)));
|
||||
if y != last_y || x != last_x + 1 || inst == 0 {
|
||||
write!(string, "{}", termion::cursor::Goto(x + 1, y + 1)).unwrap();
|
||||
inst += 1;
|
||||
}
|
||||
last_x = x;
|
||||
last_y = y;
|
||||
if cell.style.modifier != style.modifier {
|
||||
string.push_str(&cell.style.modifier.termion_modifier());
|
||||
write!(
|
||||
string,
|
||||
"{}",
|
||||
ModifierDiff {
|
||||
from: style.modifier,
|
||||
to: cell.style.modifier
|
||||
}
|
||||
)
|
||||
.unwrap();
|
||||
style.modifier = cell.style.modifier;
|
||||
if style.modifier == Modifier::Reset {
|
||||
style.bg = Color::Reset;
|
||||
style.fg = Color::Reset;
|
||||
}
|
||||
inst += 1;
|
||||
}
|
||||
if cell.style.fg != style.fg {
|
||||
string.push_str(&cell.style.fg.termion_fg());
|
||||
write!(string, "{}", Fg(cell.style.fg)).unwrap();
|
||||
style.fg = cell.style.fg;
|
||||
inst += 1;
|
||||
}
|
||||
if cell.style.bg != style.bg {
|
||||
string.push_str(&cell.style.bg.termion_bg());
|
||||
write!(string, "{}", Bg(cell.style.bg)).unwrap();
|
||||
style.bg = cell.style.bg;
|
||||
inst += 1;
|
||||
}
|
||||
@@ -134,21 +120,16 @@ where
|
||||
self.stdout,
|
||||
"{}{}{}{}",
|
||||
string,
|
||||
Color::Reset.termion_fg(),
|
||||
Color::Reset.termion_bg(),
|
||||
Modifier::Reset.termion_modifier()
|
||||
Fg(style::Color::Reset),
|
||||
Bg(style::Color::Reset),
|
||||
termion::style::Reset,
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the size of the terminal
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
let terminal = try!(termion::terminal_size());
|
||||
Ok(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: terminal.0,
|
||||
height: terminal.1,
|
||||
})
|
||||
let terminal = r#try!(termion::terminal_size());
|
||||
Ok(Rect::new(0, 0, terminal.0, terminal.1))
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
@@ -156,102 +137,131 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! termion_fg {
|
||||
($color:ident) => {
|
||||
format!("{}", termion::color::Fg(termion::color::$color))
|
||||
};
|
||||
struct Fg(style::Color);
|
||||
|
||||
struct Bg(style::Color);
|
||||
|
||||
struct ModifierDiff {
|
||||
from: style::Modifier,
|
||||
to: style::Modifier,
|
||||
}
|
||||
|
||||
macro_rules! termion_fg_rgb {
|
||||
($r:expr, $g:expr, $b:expr) => {
|
||||
format!("{}", termion::color::Fg(termion::color::Rgb($r, $g, $b)))
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! termion_bg {
|
||||
($color:ident) => {
|
||||
format!("{}", termion::color::Bg(termion::color::$color))
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! termion_bg_rgb {
|
||||
($r:expr, $g:expr, $b:expr) => {
|
||||
format!("{}", termion::color::Bg(termion::color::Rgb($r, $g, $b)))
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! termion_modifier {
|
||||
($style:ident) => {
|
||||
format!("{}", termion::style::$style)
|
||||
};
|
||||
}
|
||||
|
||||
impl Color {
|
||||
pub fn termion_fg(self) -> String {
|
||||
match self {
|
||||
Color::Reset => termion_fg!(Reset),
|
||||
Color::Black => termion_fg!(Black),
|
||||
Color::Red => termion_fg!(Red),
|
||||
Color::Green => termion_fg!(Green),
|
||||
Color::Yellow => termion_fg!(Yellow),
|
||||
Color::Blue => termion_fg!(Blue),
|
||||
Color::Magenta => termion_fg!(Magenta),
|
||||
Color::Cyan => termion_fg!(Cyan),
|
||||
Color::Gray => termion_fg!(White),
|
||||
Color::DarkGray => termion_fg!(LightBlack),
|
||||
Color::LightRed => termion_fg!(LightRed),
|
||||
Color::LightGreen => termion_fg!(LightGreen),
|
||||
Color::LightBlue => termion_fg!(LightBlue),
|
||||
Color::LightYellow => termion_fg!(LightYellow),
|
||||
Color::LightMagenta => termion_fg!(LightMagenta),
|
||||
Color::LightCyan => termion_fg!(LightCyan),
|
||||
Color::White => termion_fg!(LightWhite),
|
||||
Color::Rgb(r, g, b) => termion_fg_rgb!(r, g, b),
|
||||
impl fmt::Display for Fg {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use termion::color::Color;
|
||||
match self.0 {
|
||||
style::Color::Reset => termion::color::Reset.write_fg(f),
|
||||
style::Color::Black => termion::color::Black.write_fg(f),
|
||||
style::Color::Red => termion::color::Red.write_fg(f),
|
||||
style::Color::Green => termion::color::Green.write_fg(f),
|
||||
style::Color::Yellow => termion::color::Yellow.write_fg(f),
|
||||
style::Color::Blue => termion::color::Blue.write_fg(f),
|
||||
style::Color::Magenta => termion::color::Magenta.write_fg(f),
|
||||
style::Color::Cyan => termion::color::Cyan.write_fg(f),
|
||||
style::Color::Gray => termion::color::White.write_fg(f),
|
||||
style::Color::DarkGray => termion::color::LightBlack.write_fg(f),
|
||||
style::Color::LightRed => termion::color::LightRed.write_fg(f),
|
||||
style::Color::LightGreen => termion::color::LightGreen.write_fg(f),
|
||||
style::Color::LightBlue => termion::color::LightBlue.write_fg(f),
|
||||
style::Color::LightYellow => termion::color::LightYellow.write_fg(f),
|
||||
style::Color::LightMagenta => termion::color::LightMagenta.write_fg(f),
|
||||
style::Color::LightCyan => termion::color::LightCyan.write_fg(f),
|
||||
style::Color::White => termion::color::LightWhite.write_fg(f),
|
||||
style::Color::Indexed(i) => termion::color::AnsiValue(i).write_fg(f),
|
||||
style::Color::Rgb(r, g, b) => termion::color::Rgb(r, g, b).write_fg(f),
|
||||
}
|
||||
}
|
||||
pub fn termion_bg(self) -> String {
|
||||
match self {
|
||||
Color::Reset => termion_bg!(Reset),
|
||||
Color::Black => termion_bg!(Black),
|
||||
Color::Red => termion_bg!(Red),
|
||||
Color::Green => termion_bg!(Green),
|
||||
Color::Yellow => termion_bg!(Yellow),
|
||||
Color::Blue => termion_bg!(Blue),
|
||||
Color::Magenta => termion_bg!(Magenta),
|
||||
Color::Cyan => termion_bg!(Cyan),
|
||||
Color::Gray => termion_bg!(White),
|
||||
Color::DarkGray => termion_bg!(LightBlack),
|
||||
Color::LightRed => termion_bg!(LightRed),
|
||||
Color::LightGreen => termion_bg!(LightGreen),
|
||||
Color::LightBlue => termion_bg!(LightBlue),
|
||||
Color::LightYellow => termion_bg!(LightYellow),
|
||||
Color::LightMagenta => termion_bg!(LightMagenta),
|
||||
Color::LightCyan => termion_bg!(LightCyan),
|
||||
Color::White => termion_bg!(LightWhite),
|
||||
Color::Rgb(r, g, b) => termion_bg_rgb!(r, g, b),
|
||||
}
|
||||
impl fmt::Display for Bg {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use termion::color::Color;
|
||||
match self.0 {
|
||||
style::Color::Reset => termion::color::Reset.write_bg(f),
|
||||
style::Color::Black => termion::color::Black.write_bg(f),
|
||||
style::Color::Red => termion::color::Red.write_bg(f),
|
||||
style::Color::Green => termion::color::Green.write_bg(f),
|
||||
style::Color::Yellow => termion::color::Yellow.write_bg(f),
|
||||
style::Color::Blue => termion::color::Blue.write_bg(f),
|
||||
style::Color::Magenta => termion::color::Magenta.write_bg(f),
|
||||
style::Color::Cyan => termion::color::Cyan.write_bg(f),
|
||||
style::Color::Gray => termion::color::White.write_bg(f),
|
||||
style::Color::DarkGray => termion::color::LightBlack.write_bg(f),
|
||||
style::Color::LightRed => termion::color::LightRed.write_bg(f),
|
||||
style::Color::LightGreen => termion::color::LightGreen.write_bg(f),
|
||||
style::Color::LightBlue => termion::color::LightBlue.write_bg(f),
|
||||
style::Color::LightYellow => termion::color::LightYellow.write_bg(f),
|
||||
style::Color::LightMagenta => termion::color::LightMagenta.write_bg(f),
|
||||
style::Color::LightCyan => termion::color::LightCyan.write_bg(f),
|
||||
style::Color::White => termion::color::LightWhite.write_bg(f),
|
||||
style::Color::Indexed(i) => termion::color::AnsiValue(i).write_bg(f),
|
||||
style::Color::Rgb(r, g, b) => termion::color::Rgb(r, g, b).write_bg(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Modifier {
|
||||
pub fn termion_modifier(self) -> String {
|
||||
match self {
|
||||
Modifier::Blink => termion_modifier!(Blink),
|
||||
Modifier::Bold => termion_modifier!(Bold),
|
||||
Modifier::CrossedOut => termion_modifier!(CrossedOut),
|
||||
Modifier::Faint => termion_modifier!(Faint),
|
||||
Modifier::Framed => termion_modifier!(Framed),
|
||||
Modifier::Invert => termion_modifier!(Invert),
|
||||
Modifier::Italic => termion_modifier!(Italic),
|
||||
Modifier::NoBlink => termion_modifier!(NoBlink),
|
||||
Modifier::NoBold => termion_modifier!(NoBold),
|
||||
Modifier::NoCrossedOut => termion_modifier!(NoCrossedOut),
|
||||
Modifier::NoFaint => termion_modifier!(NoFaint),
|
||||
Modifier::NoInvert => termion_modifier!(NoInvert),
|
||||
Modifier::NoItalic => termion_modifier!(NoItalic),
|
||||
Modifier::NoUnderline => termion_modifier!(NoUnderline),
|
||||
Modifier::Reset => termion_modifier!(Reset),
|
||||
Modifier::Underline => termion_modifier!(Underline),
|
||||
impl fmt::Display for ModifierDiff {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let remove = self.from - self.to;
|
||||
if remove.contains(style::Modifier::REVERSED) {
|
||||
write!(f, "{}", termion::style::NoInvert)?;
|
||||
}
|
||||
if remove.contains(style::Modifier::BOLD) {
|
||||
// XXX: the termion NoBold flag actually enables double-underline on ECMA-48 compliant
|
||||
// terminals, and NoFaint additionally disables bold... so we use this trick to get
|
||||
// the right semantics.
|
||||
write!(f, "{}", termion::style::NoFaint)?;
|
||||
|
||||
if self.to.contains(style::Modifier::DIM) {
|
||||
write!(f, "{}", termion::style::Faint)?;
|
||||
}
|
||||
}
|
||||
if remove.contains(style::Modifier::ITALIC) {
|
||||
write!(f, "{}", termion::style::NoItalic)?;
|
||||
}
|
||||
if remove.contains(style::Modifier::UNDERLINED) {
|
||||
write!(f, "{}", termion::style::NoUnderline)?;
|
||||
}
|
||||
if remove.contains(style::Modifier::DIM) {
|
||||
write!(f, "{}", termion::style::NoFaint)?;
|
||||
|
||||
// XXX: the NoFaint flag additionally disables bold as well, so we need to re-enable it
|
||||
// here if we want it.
|
||||
if self.to.contains(style::Modifier::BOLD) {
|
||||
write!(f, "{}", termion::style::Bold)?;
|
||||
}
|
||||
}
|
||||
if remove.contains(style::Modifier::CROSSED_OUT) {
|
||||
write!(f, "{}", termion::style::NoCrossedOut)?;
|
||||
}
|
||||
if remove.contains(style::Modifier::SLOW_BLINK)
|
||||
|| remove.contains(style::Modifier::RAPID_BLINK)
|
||||
{
|
||||
write!(f, "{}", termion::style::NoBlink)?;
|
||||
}
|
||||
|
||||
let add = self.to - self.from;
|
||||
if add.contains(style::Modifier::REVERSED) {
|
||||
write!(f, "{}", termion::style::Invert)?;
|
||||
}
|
||||
if add.contains(style::Modifier::BOLD) {
|
||||
write!(f, "{}", termion::style::Bold)?;
|
||||
}
|
||||
if add.contains(style::Modifier::ITALIC) {
|
||||
write!(f, "{}", termion::style::Italic)?;
|
||||
}
|
||||
if add.contains(style::Modifier::UNDERLINED) {
|
||||
write!(f, "{}", termion::style::Underline)?;
|
||||
}
|
||||
if add.contains(style::Modifier::DIM) {
|
||||
write!(f, "{}", termion::style::Faint)?;
|
||||
}
|
||||
if add.contains(style::Modifier::CROSSED_OUT) {
|
||||
write!(f, "{}", termion::style::CrossedOut)?;
|
||||
}
|
||||
if add.contains(style::Modifier::SLOW_BLINK) || add.contains(style::Modifier::RAPID_BLINK) {
|
||||
write!(f, "{}", termion::style::Blink)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
67
src/backend/test.rs
Normal file
67
src/backend/test.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use crate::backend::Backend;
|
||||
use crate::buffer::{Buffer, Cell};
|
||||
use crate::layout::Rect;
|
||||
use std::io;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TestBackend {
|
||||
width: u16,
|
||||
buffer: Buffer,
|
||||
height: u16,
|
||||
cursor: bool,
|
||||
pos: (u16, u16),
|
||||
}
|
||||
|
||||
impl TestBackend {
|
||||
pub fn new(width: u16, height: u16) -> TestBackend {
|
||||
TestBackend {
|
||||
width,
|
||||
height,
|
||||
buffer: Buffer::empty(Rect::new(0, 0, width, height)),
|
||||
cursor: false,
|
||||
pos: (0, 0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer(&self) -> &Buffer {
|
||||
&self.buffer
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for TestBackend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
for (x, y, c) in content {
|
||||
let cell = self.buffer.get_mut(x, y);
|
||||
cell.symbol = c.symbol.clone();
|
||||
cell.style = c.style;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||
self.cursor = false;
|
||||
Ok(())
|
||||
}
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error> {
|
||||
self.cursor = true;
|
||||
Ok(())
|
||||
}
|
||||
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error> {
|
||||
Ok(self.pos)
|
||||
}
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error> {
|
||||
self.pos = (x, y);
|
||||
Ok(())
|
||||
}
|
||||
fn clear(&mut self) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
Ok(Rect::new(0, 0, self.width, self.height))
|
||||
}
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
410
src/buffer.rs
410
src/buffer.rs
@@ -1,10 +1,12 @@
|
||||
use std::cmp::min;
|
||||
use std::fmt;
|
||||
use std::usize;
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use layout::Rect;
|
||||
use style::{Color, Modifier, Style};
|
||||
use crate::layout::Rect;
|
||||
use crate::style::{Color, Modifier, Style};
|
||||
|
||||
/// A buffer cell
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -72,7 +74,6 @@ impl Default for Cell {
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// use tui::buffer::{Buffer, Cell};
|
||||
/// use tui::layout::Rect;
|
||||
/// use tui::style::{Color, Style, Modifier};
|
||||
@@ -81,19 +82,19 @@ impl Default for Cell {
|
||||
/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
|
||||
/// buf.get_mut(0, 2).set_symbol("x");
|
||||
/// assert_eq!(buf.get(0, 2).symbol, "x");
|
||||
/// buf.set_string(3, 0, "string", &Style::default().fg(Color::Red).bg(Color::White));
|
||||
/// buf.set_string(3, 0, "string", Style::default().fg(Color::Red).bg(Color::White));
|
||||
/// assert_eq!(buf.get(5, 0), &Cell{
|
||||
/// symbol: String::from("r"),
|
||||
/// style: Style {
|
||||
/// fg: Color::Red,
|
||||
/// bg: Color::White,
|
||||
/// modifier: Modifier::Reset
|
||||
/// modifier: Modifier::empty()
|
||||
/// }});
|
||||
/// buf.get_mut(5, 0).set_char('x');
|
||||
/// assert_eq!(buf.get(5, 0).symbol, "x");
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct Buffer {
|
||||
/// The area represented by this buffer
|
||||
pub area: Rect,
|
||||
@@ -111,6 +112,49 @@ impl Default for Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Buffer {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(f, "Buffer: {:?}", self.area)?;
|
||||
f.write_str("Content (quoted lines):\n")?;
|
||||
for cells in self.content.chunks(self.area.width as usize) {
|
||||
let mut line = String::new();
|
||||
let mut overwritten = vec![];
|
||||
let mut skip: usize = 0;
|
||||
for (x, c) in cells.iter().enumerate() {
|
||||
if skip == 0 {
|
||||
line.push_str(&c.symbol);
|
||||
} else {
|
||||
overwritten.push((x, &c.symbol))
|
||||
}
|
||||
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
|
||||
}
|
||||
f.write_fmt(format_args!("{:?},", line))?;
|
||||
if !overwritten.is_empty() {
|
||||
f.write_fmt(format_args!(
|
||||
" Hidden by multi-width symbols: {:?}",
|
||||
overwritten
|
||||
))?;
|
||||
}
|
||||
f.write_str("\n")?;
|
||||
}
|
||||
f.write_str("Style:\n")?;
|
||||
for cells in self.content.chunks(self.area.width as usize) {
|
||||
f.write_str("|")?;
|
||||
for cell in cells {
|
||||
write!(
|
||||
f,
|
||||
"{} {} {}|",
|
||||
cell.style.fg.code(),
|
||||
cell.style.bg.code(),
|
||||
cell.style.modifier.code()
|
||||
)?;
|
||||
}
|
||||
f.write_str("\n")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
/// Returns a Buffer with all cells set to the default one
|
||||
pub fn empty(area: Rect) -> Buffer {
|
||||
@@ -128,6 +172,27 @@ impl Buffer {
|
||||
Buffer { area, content }
|
||||
}
|
||||
|
||||
/// Returns a Buffer containing the given lines
|
||||
pub fn with_lines<S>(lines: Vec<S>) -> Buffer
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let height = lines.len() as u16;
|
||||
let width = lines.iter().fold(0, |acc, item| {
|
||||
std::cmp::max(acc, item.as_ref().width() as u16)
|
||||
});
|
||||
let mut buffer = Buffer::empty(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
for (y, line) in lines.iter().enumerate() {
|
||||
buffer.set_string(0, y as u16, line, Style::default());
|
||||
}
|
||||
buffer
|
||||
}
|
||||
|
||||
/// Returns the content of the buffer as a slice
|
||||
pub fn content(&self) -> &[Cell] {
|
||||
&self.content
|
||||
@@ -233,21 +298,39 @@ impl Buffer {
|
||||
}
|
||||
|
||||
/// Print a string, starting at the position (x, y)
|
||||
pub fn set_string(&mut self, x: u16, y: u16, string: &str, style: &Style) {
|
||||
pub fn set_string<S>(&mut self, x: u16, y: u16, string: S, style: Style)
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
self.set_stringn(x, y, string, usize::MAX, style);
|
||||
}
|
||||
|
||||
/// Print at most the first n characters of a string if enough space is available
|
||||
/// until the end of the line
|
||||
pub fn set_stringn(&mut self, x: u16, y: u16, string: &str, limit: usize, style: &Style) {
|
||||
pub fn set_stringn<S>(&mut self, x: u16, y: u16, string: S, width: usize, style: Style)
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let mut index = self.index_of(x, y);
|
||||
let graphemes = UnicodeSegmentation::graphemes(string, true).collect::<Vec<&str>>();
|
||||
let max_index = min((self.area.right() - x) as usize, limit);
|
||||
for s in graphemes.into_iter().take(max_index) {
|
||||
self.content[index].symbol.clear();
|
||||
self.content[index].symbol.push_str(s);
|
||||
self.content[index].style = *style;
|
||||
index += 1;
|
||||
let mut x_offset = x as usize;
|
||||
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
|
||||
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
|
||||
for s in graphemes {
|
||||
let width = s.width();
|
||||
// `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
|
||||
// change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32.
|
||||
if width > max_offset.saturating_sub(x_offset) {
|
||||
break;
|
||||
}
|
||||
|
||||
self.content[index].set_symbol(s);
|
||||
self.content[index].set_style(style);
|
||||
// Reset following cells if multi-width (they would be hidden by the grapheme),
|
||||
for i in index + 1..index + width {
|
||||
self.content[i].reset();
|
||||
}
|
||||
index += width;
|
||||
x_offset += width;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,38 +360,94 @@ impl Buffer {
|
||||
self.content.resize(area.area() as usize, cell.clone());
|
||||
|
||||
// Move original content to the appropriate space
|
||||
let offset_x = self.area.x - area.x;
|
||||
let offset_y = self.area.y - area.y;
|
||||
let size = self.area.area() as usize;
|
||||
for i in (0..size).rev() {
|
||||
let (x, y) = self.pos_of(i);
|
||||
// New index in content
|
||||
let k = ((y + offset_y) * area.width + (x + offset_x)) as usize;
|
||||
self.content[k] = self.content[i].clone();
|
||||
let k = ((y - area.y) * area.width + x - area.x) as usize;
|
||||
if i != k {
|
||||
self.content[k] = self.content[i].clone();
|
||||
self.content[i] = cell.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// Push content of the other buffer into this one (may erase previous
|
||||
// data)
|
||||
let offset_x = other.area.x - area.x;
|
||||
let offset_y = other.area.y - area.y;
|
||||
let size = other.area.area() as usize;
|
||||
for i in 0..size {
|
||||
let (x, y) = other.pos_of(i);
|
||||
// New index in content
|
||||
let k = ((y + offset_y) * area.width + (x + offset_x)) as usize;
|
||||
let k = ((y - area.y) * area.width + x - area.x) as usize;
|
||||
self.content[k] = other.content[i].clone();
|
||||
}
|
||||
self.area = area;
|
||||
}
|
||||
|
||||
/// Builds a minimal sequence of coordinates and Cells necessary to update the UI from
|
||||
/// self to other.
|
||||
///
|
||||
/// We're assuming that buffers are well-formed, that is no double-width cell is followed by
|
||||
/// a non-blank cell.
|
||||
///
|
||||
/// # Multi-width characters handling:
|
||||
///
|
||||
/// ```text
|
||||
/// (Index:) `01`
|
||||
/// Prev: `コ`
|
||||
/// Next: `aa`
|
||||
/// Updates: `0: a, 1: a'
|
||||
/// ```
|
||||
///
|
||||
/// ```text
|
||||
/// (Index:) `01`
|
||||
/// Prev: `a `
|
||||
/// Next: `コ`
|
||||
/// Updates: `0: コ` (double width symbol at index 0 - skip index 1)
|
||||
/// ```
|
||||
///
|
||||
/// ```text
|
||||
/// (Index:) `012`
|
||||
/// Prev: `aaa`
|
||||
/// Next: `aコ`
|
||||
/// Updates: `0: a, 1: コ` (double width symbol at index 1 - skip index 2)
|
||||
/// ```
|
||||
pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> {
|
||||
let previous_buffer = &self.content;
|
||||
let next_buffer = &other.content;
|
||||
let width = self.area.width;
|
||||
|
||||
let mut updates: Vec<(u16, u16, &Cell)> = vec![];
|
||||
// Cells invalidated by drawing/replacing preceeding multi-width characters:
|
||||
let mut invalidated: usize = 0;
|
||||
// Cells from the current buffer to skip due to preceeding multi-width characters taking their
|
||||
// place (the skipped cells should be blank anyway):
|
||||
let mut to_skip: usize = 0;
|
||||
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
|
||||
if (current != previous || invalidated > 0) && to_skip == 0 {
|
||||
let x = i as u16 % width;
|
||||
let y = i as u16 / width;
|
||||
updates.push((x, y, &next_buffer[i]));
|
||||
}
|
||||
|
||||
to_skip = current.symbol.width().saturating_sub(1);
|
||||
|
||||
let affected_width = std::cmp::max(current.symbol.width(), previous.symbol.width());
|
||||
invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
|
||||
}
|
||||
updates
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn cell(s: &str) -> Cell {
|
||||
let mut cell = Cell::default();
|
||||
cell.set_symbol(s);
|
||||
cell
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_translates_to_and_from_coordinates() {
|
||||
let rect = Rect::new(200, 100, 50, 80);
|
||||
@@ -342,4 +481,231 @@ mod tests {
|
||||
// width is 10; zero-indexed means that 10 would be the 11th cell.
|
||||
buf.index_of(10, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_set_string() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
// Zero-width
|
||||
buffer.set_stringn(0, 0, "aaa", 0, Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec![" "]));
|
||||
|
||||
buffer.set_string(0, 0, "aaa", Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
|
||||
|
||||
// Width limit:
|
||||
buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
|
||||
|
||||
buffer.set_string(0, 0, "12345", Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
|
||||
// Width truncation:
|
||||
buffer.set_string(0, 0, "123456", Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_set_string_double_width() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
buffer.set_string(0, 0, "コン", Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
|
||||
// Only 1 space left.
|
||||
buffer.set_string(0, 0, "コンピ", Style::default());
|
||||
assert_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_with_lines() {
|
||||
let buffer = Buffer::with_lines(vec![
|
||||
"┌────────┐",
|
||||
"│コンピュ│",
|
||||
"│ーa 上で│",
|
||||
"└────────┘",
|
||||
]);
|
||||
assert_eq!(buffer.area.x, 0);
|
||||
assert_eq!(buffer.area.y, 0);
|
||||
assert_eq!(buffer.area.width, 10);
|
||||
assert_eq!(buffer.area.height, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_diffing_empty_empty() {
|
||||
let area = Rect::new(0, 0, 40, 40);
|
||||
let prev = Buffer::empty(area);
|
||||
let next = Buffer::empty(area);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_diffing_empty_filled() {
|
||||
let area = Rect::new(0, 0, 40, 40);
|
||||
let prev = Buffer::empty(area);
|
||||
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff.len(), 40 * 40);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_diffing_filled_filled() {
|
||||
let area = Rect::new(0, 0, 40, 40);
|
||||
let prev = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_diffing_single_width() {
|
||||
let prev = Buffer::with_lines(vec![
|
||||
" ",
|
||||
"┌Title─┐ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let next = Buffer::with_lines(vec![
|
||||
" ",
|
||||
"┌TITLE─┐ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![
|
||||
(2, 1, &cell("I")),
|
||||
(3, 1, &cell("T")),
|
||||
(4, 1, &cell("L")),
|
||||
(5, 1, &cell("E")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[rustfmt::skip]
|
||||
fn buffer_diffing_multi_width() {
|
||||
let prev = Buffer::with_lines(vec![
|
||||
"┌Title─┐ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let next = Buffer::with_lines(vec![
|
||||
"┌称号──┐ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![
|
||||
(1, 0, &cell("称")),
|
||||
// Skipped "i"
|
||||
(3, 0, &cell("号")),
|
||||
// Skipped "l"
|
||||
(5, 0, &cell("─")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_diffing_multi_width_offset() {
|
||||
let prev = Buffer::with_lines(vec!["┌称号──┐"]);
|
||||
let next = Buffer::with_lines(vec!["┌─称号─┐"]);
|
||||
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![
|
||||
(1, 0, &cell("─")),
|
||||
(2, 0, &cell("称")),
|
||||
(4, 0, &cell("号")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_merge() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 2,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
assert_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_merge2() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 2,
|
||||
y: 2,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
assert_eq!(
|
||||
one,
|
||||
Buffer::with_lines(vec!["22 ", "22 ", " 11", " 11"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_merge3() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 3,
|
||||
y: 3,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 3,
|
||||
height: 4,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
let mut merged = Buffer::with_lines(vec!["222 ", "222 ", "2221", "2221"]);
|
||||
merged.area = Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 4,
|
||||
height: 4,
|
||||
};
|
||||
assert_eq!(one, merged);
|
||||
}
|
||||
}
|
||||
|
||||
132
src/layout.rs
132
src/layout.rs
@@ -24,6 +24,7 @@ pub enum Direction {
|
||||
pub enum Constraint {
|
||||
// TODO: enforce range 0 - 100
|
||||
Percentage(u16),
|
||||
Ratio(u32, u32),
|
||||
Length(u16),
|
||||
Max(u16),
|
||||
Min(u16),
|
||||
@@ -82,16 +83,62 @@ impl Layout {
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// # use tui::layout::{Rect, Constraint, Direction, Layout};
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let chunks = Layout::default()
|
||||
/// .direction(Direction::Vertical)
|
||||
/// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref())
|
||||
/// .split(Rect{x: 2, y: 2, width: 10, height: 10});
|
||||
/// assert_eq!(chunks, vec![Rect{x:2, y: 2, width: 10, height: 5},
|
||||
/// Rect{x: 2, y: 7, width: 10, height: 5}])
|
||||
/// let chunks = Layout::default()
|
||||
/// .direction(Direction::Vertical)
|
||||
/// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref())
|
||||
/// .split(Rect {
|
||||
/// x: 2,
|
||||
/// y: 2,
|
||||
/// width: 10,
|
||||
/// height: 10,
|
||||
/// });
|
||||
/// assert_eq!(
|
||||
/// chunks,
|
||||
/// vec![
|
||||
/// Rect {
|
||||
/// x: 2,
|
||||
/// y: 2,
|
||||
/// width: 10,
|
||||
/// height: 5
|
||||
/// },
|
||||
/// Rect {
|
||||
/// x: 2,
|
||||
/// y: 7,
|
||||
/// width: 10,
|
||||
/// height: 5
|
||||
/// }
|
||||
/// ]
|
||||
/// );
|
||||
///
|
||||
/// let chunks = Layout::default()
|
||||
/// .direction(Direction::Horizontal)
|
||||
/// .constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)].as_ref())
|
||||
/// .split(Rect {
|
||||
/// x: 0,
|
||||
/// y: 0,
|
||||
/// width: 9,
|
||||
/// height: 2,
|
||||
/// });
|
||||
/// assert_eq!(
|
||||
/// chunks,
|
||||
/// vec![
|
||||
/// Rect {
|
||||
/// x: 0,
|
||||
/// y: 0,
|
||||
/// width: 3,
|
||||
/// height: 2
|
||||
/// },
|
||||
/// Rect {
|
||||
/// x: 3,
|
||||
/// y: 0,
|
||||
/// width: 6,
|
||||
/// height: 2
|
||||
/// }
|
||||
/// ]
|
||||
/// );
|
||||
/// # }
|
||||
///
|
||||
/// ```
|
||||
@@ -160,6 +207,11 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
|
||||
Constraint::Percentage(v) => {
|
||||
elements[i].width | EQ(WEAK) | (f64::from(v * dest_area.width) / 100.0)
|
||||
}
|
||||
Constraint::Ratio(n, d) => {
|
||||
elements[i].width
|
||||
| EQ(WEAK)
|
||||
| (f64::from(dest_area.width) * f64::from(n) / f64::from(d))
|
||||
}
|
||||
Constraint::Min(v) => elements[i].width | GE(WEAK) | f64::from(v),
|
||||
Constraint::Max(v) => elements[i].width | LE(WEAK) | f64::from(v),
|
||||
});
|
||||
@@ -177,6 +229,11 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
|
||||
Constraint::Percentage(v) => {
|
||||
elements[i].height | EQ(WEAK) | (f64::from(v * dest_area.height) / 100.0)
|
||||
}
|
||||
Constraint::Ratio(n, d) => {
|
||||
elements[i].height
|
||||
| EQ(WEAK)
|
||||
| (f64::from(dest_area.height) * f64::from(n) / f64::from(d))
|
||||
}
|
||||
Constraint::Min(v) => elements[i].height | GE(WEAK) | f64::from(v),
|
||||
Constraint::Max(v) => elements[i].height | LE(WEAK) | f64::from(v),
|
||||
});
|
||||
@@ -279,12 +336,25 @@ impl Default for Rect {
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
/// Creates a new rect, with width and height limited to keep the area under max u16.
|
||||
/// If clipped, aspect ratio will be preserved.
|
||||
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
|
||||
let max_area = u16::max_value();
|
||||
let (clipped_width, clipped_height) =
|
||||
if u32::from(width) * u32::from(height) > u32::from(max_area) {
|
||||
let aspect_ratio = f64::from(width) / f64::from(height);
|
||||
let max_area_f = f64::from(max_area);
|
||||
let height_f = (max_area_f / aspect_ratio).sqrt();
|
||||
let width_f = height_f * aspect_ratio;
|
||||
(width_f as u16, height_f as u16)
|
||||
} else {
|
||||
(width, height)
|
||||
};
|
||||
Rect {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
width: clipped_width,
|
||||
height: clipped_height,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,3 +424,47 @@ impl Rect {
|
||||
&& self.y + self.height > other.y
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rect_size_truncation() {
|
||||
for width in 256u16..300u16 {
|
||||
for height in 256u16..300u16 {
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
rect.area(); // Should not panic.
|
||||
assert!(rect.width < width || rect.height < height);
|
||||
// The target dimensions are rounded down so the math will not be too precise
|
||||
// but let's make sure the ratios don't diverge crazily.
|
||||
assert!(
|
||||
(f64::from(rect.width) / f64::from(rect.height)
|
||||
- f64::from(width) / f64::from(height))
|
||||
.abs()
|
||||
< 1.0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// One dimension below 255, one above. Area above max u16.
|
||||
let width = 900;
|
||||
let height = 100;
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
assert_ne!(rect.width, 900);
|
||||
assert_ne!(rect.height, 100);
|
||||
assert!(rect.width < width || rect.height < height);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rect_size_preservation() {
|
||||
for width in 0..256u16 {
|
||||
for height in 0..256u16 {
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
rect.area(); // Should not panic.
|
||||
assert_eq!(rect.width, width);
|
||||
assert_eq!(rect.height, height);
|
||||
}
|
||||
}
|
||||
|
||||
// One dimension below 255, one above. Area below max u16.
|
||||
let rect = Rect::new(0, 0, 300, 100);
|
||||
assert_eq!(rect.width, 300);
|
||||
assert_eq!(rect.height, 100);
|
||||
}
|
||||
|
||||
87
src/lib.rs
87
src/lib.rs
@@ -1,7 +1,7 @@
|
||||
//! [tui](https://github.com/fdehau/tui-rs) is a library used to build rich
|
||||
//! terminal users interfaces and dashboards.
|
||||
//!
|
||||
//! 
|
||||
//! 
|
||||
//!
|
||||
//! # Get started
|
||||
//!
|
||||
@@ -13,14 +13,16 @@
|
||||
//! backend is available.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! extern crate tui;
|
||||
//!
|
||||
//! use std::io;
|
||||
//! use tui::Terminal;
|
||||
//! use tui::backend::RawBackend;
|
||||
//! use tui::backend::TermionBackend;
|
||||
//! use termion::raw::IntoRawMode;
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let backend = RawBackend::new().unwrap();
|
||||
//! let mut terminal = Terminal::new(backend).unwrap();
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! let stdout = io::stdout().into_raw_mode()?;
|
||||
//! let backend = TermionBackend::new(stdout);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
@@ -29,7 +31,7 @@
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies.tui]
|
||||
//! version = "0.2.0"
|
||||
//! version = "0.3.0"
|
||||
//! default-features = false
|
||||
//! features = ['rustbox']
|
||||
//! ```
|
||||
@@ -37,14 +39,14 @@
|
||||
//! and then create the terminal in a similar way:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! extern crate tui;
|
||||
//!
|
||||
//! use tui::Terminal;
|
||||
//! use tui::backend::RustboxBackend;
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let backend = RustboxBackend::new().unwrap();
|
||||
//! let mut terminal = Terminal::new(backend).unwrap();
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! let backend = RustboxBackend::new()?;
|
||||
//! let mut terminal = Terminal::new(backend);
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
@@ -62,29 +64,20 @@
|
||||
//! The following example renders a block of the size of the terminal:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! extern crate tui;
|
||||
//!
|
||||
//! use std::io;
|
||||
//!
|
||||
//! use termion::raw::IntoRawMode;
|
||||
//! use tui::Terminal;
|
||||
//! use tui::backend::RawBackend;
|
||||
//! use tui::backend::TermionBackend;
|
||||
//! use tui::widgets::{Widget, Block, Borders};
|
||||
//! use tui::layout::{Layout, Constraint, Direction};
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let mut terminal = init().expect("Failed initialization");
|
||||
//! draw(&mut terminal).expect("Failed to draw");
|
||||
//! }
|
||||
//!
|
||||
//! fn init() -> Result<Terminal<RawBackend>, io::Error> {
|
||||
//! let backend = RawBackend::new()?;
|
||||
//! Terminal::new(backend)
|
||||
//! }
|
||||
//!
|
||||
//! fn draw(t: &mut Terminal<RawBackend>) -> Result<(), io::Error> {
|
||||
//!
|
||||
//! let size = t.size()?;
|
||||
//! t.draw(|mut f| {
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! let stdout = io::stdout().into_raw_mode()?;
|
||||
//! let backend = TermionBackend::new(stdout);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! terminal.draw(|mut f| {
|
||||
//! let size = f.size();
|
||||
//! Block::default()
|
||||
//! .title("Block")
|
||||
//! .borders(Borders::ALL)
|
||||
@@ -100,29 +93,19 @@
|
||||
//! full customization. And `Layout` is no exception:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! extern crate tui;
|
||||
//!
|
||||
//! use std::io;
|
||||
//!
|
||||
//! use termion::raw::IntoRawMode;
|
||||
//! use tui::Terminal;
|
||||
//! use tui::backend::RawBackend;
|
||||
//! use tui::backend::TermionBackend;
|
||||
//! use tui::widgets::{Widget, Block, Borders};
|
||||
//! use tui::layout::{Layout, Constraint, Direction};
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let mut terminal = init().expect("Failed initialization");
|
||||
//! draw(&mut terminal).expect("Failed to draw");
|
||||
//! }
|
||||
//!
|
||||
//! fn init() -> Result<Terminal<RawBackend>, io::Error> {
|
||||
//! let backend = RawBackend::new()?;
|
||||
//! Terminal::new(backend)
|
||||
//! }
|
||||
//!
|
||||
//! fn draw(t: &mut Terminal<RawBackend>) -> Result<(), io::Error> {
|
||||
//!
|
||||
//! let size = t.size()?;
|
||||
//! t.draw(|mut f| {
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! let stdout = io::stdout().into_raw_mode()?;
|
||||
//! let backend = TermionBackend::new(stdout);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! terminal.draw(|mut f| {
|
||||
//! let chunks = Layout::default()
|
||||
//! .direction(Direction::Vertical)
|
||||
//! .margin(1)
|
||||
@@ -133,7 +116,7 @@
|
||||
//! Constraint::Percentage(10)
|
||||
//! ].as_ref()
|
||||
//! )
|
||||
//! .split(size);
|
||||
//! .split(f.size());
|
||||
//! Block::default()
|
||||
//! .title("Block")
|
||||
//! .borders(Borders::ALL)
|
||||
@@ -151,16 +134,6 @@
|
||||
//! completely. So if for any reason you might need a blank space somewhere, try to
|
||||
//! pass an additional constraint and don't use the corresponding area.
|
||||
|
||||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
extern crate cassowary;
|
||||
extern crate either;
|
||||
extern crate itertools;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate unicode_segmentation;
|
||||
extern crate unicode_width;
|
||||
|
||||
pub mod backend;
|
||||
pub mod buffer;
|
||||
pub mod layout;
|
||||
|
||||
105
src/style.rs
105
src/style.rs
@@ -1,3 +1,5 @@
|
||||
use bitflags::bitflags;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Color {
|
||||
Reset,
|
||||
@@ -18,26 +20,89 @@ pub enum Color {
|
||||
LightCyan,
|
||||
White,
|
||||
Rgb(u8, u8, u8),
|
||||
Indexed(u8),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Modifier {
|
||||
Blink,
|
||||
Bold,
|
||||
CrossedOut,
|
||||
Faint,
|
||||
Framed,
|
||||
Invert,
|
||||
Italic,
|
||||
NoBlink,
|
||||
NoBold,
|
||||
NoCrossedOut,
|
||||
NoFaint,
|
||||
NoInvert,
|
||||
NoItalic,
|
||||
NoUnderline,
|
||||
Reset,
|
||||
Underline,
|
||||
impl Color {
|
||||
/// Returns a short code associated with the color, used for debug purpose
|
||||
/// only
|
||||
pub(crate) fn code(&self) -> &str {
|
||||
match self {
|
||||
Color::Reset => "X",
|
||||
Color::Black => "b",
|
||||
Color::Red => "r",
|
||||
Color::Green => "c",
|
||||
Color::Yellow => "y",
|
||||
Color::Blue => "b",
|
||||
Color::Magenta => "m",
|
||||
Color::Cyan => "c",
|
||||
Color::Gray => "w",
|
||||
Color::DarkGray => "B",
|
||||
Color::LightRed => "R",
|
||||
Color::LightGreen => "G",
|
||||
Color::LightYellow => "Y",
|
||||
Color::LightBlue => "B",
|
||||
Color::LightMagenta => "M",
|
||||
Color::LightCyan => "C",
|
||||
Color::White => "W",
|
||||
Color::Indexed(_) => "i",
|
||||
Color::Rgb(_, _, _) => "o",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
pub struct Modifier: u16 {
|
||||
const BOLD = 0b0000_0000_0001;
|
||||
const DIM = 0b0000_0000_0010;
|
||||
const ITALIC = 0b0000_0000_0100;
|
||||
const UNDERLINED = 0b0000_0000_1000;
|
||||
const SLOW_BLINK = 0b0000_0001_0000;
|
||||
const RAPID_BLINK = 0b0000_0010_0000;
|
||||
const REVERSED = 0b0000_0100_0000;
|
||||
const HIDDEN = 0b0000_1000_0000;
|
||||
const CROSSED_OUT = 0b0001_0000_0000;
|
||||
}
|
||||
}
|
||||
|
||||
impl Modifier {
|
||||
/// Returns a short code associated with the color, used for debug purpose
|
||||
/// only
|
||||
pub(crate) fn code(&self) -> String {
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut result = String::new();
|
||||
|
||||
if self.contains(Modifier::BOLD) {
|
||||
write!(result, "BO").unwrap();
|
||||
}
|
||||
if self.contains(Modifier::DIM) {
|
||||
write!(result, "DI").unwrap();
|
||||
}
|
||||
if self.contains(Modifier::ITALIC) {
|
||||
write!(result, "IT").unwrap();
|
||||
}
|
||||
if self.contains(Modifier::UNDERLINED) {
|
||||
write!(result, "UN").unwrap();
|
||||
}
|
||||
if self.contains(Modifier::SLOW_BLINK) {
|
||||
write!(result, "SL").unwrap();
|
||||
}
|
||||
if self.contains(Modifier::RAPID_BLINK) {
|
||||
write!(result, "RA").unwrap();
|
||||
}
|
||||
if self.contains(Modifier::REVERSED) {
|
||||
write!(result, "RE").unwrap();
|
||||
}
|
||||
if self.contains(Modifier::HIDDEN) {
|
||||
write!(result, "HI").unwrap();
|
||||
}
|
||||
if self.contains(Modifier::CROSSED_OUT) {
|
||||
write!(result, "CR").unwrap();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
@@ -52,7 +117,7 @@ impl Default for Style {
|
||||
Style {
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
modifier: Modifier::Reset,
|
||||
modifier: Modifier::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,7 +126,7 @@ impl Style {
|
||||
pub fn reset(&mut self) {
|
||||
self.fg = Color::Reset;
|
||||
self.bg = Color::Reset;
|
||||
self.modifier = Modifier::Reset;
|
||||
self.modifier = Modifier::empty();
|
||||
}
|
||||
|
||||
pub fn fg(mut self, color: Color) -> Style {
|
||||
|
||||
114
src/terminal.rs
114
src/terminal.rs
@@ -1,9 +1,10 @@
|
||||
use log::error;
|
||||
use std::io;
|
||||
|
||||
use backend::Backend;
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
use widgets::Widget;
|
||||
use crate::backend::Backend;
|
||||
use crate::buffer::Buffer;
|
||||
use crate::layout::Rect;
|
||||
use crate::widgets::Widget;
|
||||
|
||||
/// Interface to the terminal backed by Termion
|
||||
#[derive(Debug)]
|
||||
@@ -17,8 +18,13 @@ where
|
||||
buffers: [Buffer; 2],
|
||||
/// Index of the current buffer in the previous array
|
||||
current: usize,
|
||||
/// Whether the cursor is currently hidden
|
||||
hidden_cursor: bool,
|
||||
/// Terminal size used for rendering.
|
||||
known_size: Rect,
|
||||
}
|
||||
|
||||
/// Represents a consistent terminal interface for rendering.
|
||||
pub struct Frame<'a, B: 'a>
|
||||
where
|
||||
B: Backend,
|
||||
@@ -30,6 +36,11 @@ impl<'a, B> Frame<'a, B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// Terminal size, guaranteed not to change when rendering.
|
||||
pub fn size(&self) -> Rect {
|
||||
self.terminal.known_size
|
||||
}
|
||||
|
||||
/// Calls the draw method of a given widget on the current buffer
|
||||
pub fn render<W>(&mut self, widget: &mut W, area: Rect)
|
||||
where
|
||||
@@ -39,21 +50,38 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> Drop for Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
// Attempt to restore the cursor state
|
||||
if self.hidden_cursor {
|
||||
if let Err(err) = self.show_cursor() {
|
||||
error!("Failed to show the cursor: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// Wrapper around Termion initialization. Each buffer is initialized with a blank string and
|
||||
/// default colors for the foreground and the background
|
||||
pub fn new(backend: B) -> Result<Terminal<B>, io::Error> {
|
||||
pub fn new(backend: B) -> io::Result<Terminal<B>> {
|
||||
let size = backend.size()?;
|
||||
Ok(Terminal {
|
||||
backend,
|
||||
buffers: [Buffer::empty(size), Buffer::empty(size)],
|
||||
current: 0,
|
||||
hidden_cursor: false,
|
||||
known_size: size,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a Frame object which provides a consistent view into the terminal state for rendering.
|
||||
pub fn get_frame(&mut self) -> Frame<B> {
|
||||
Frame { terminal: self }
|
||||
}
|
||||
@@ -70,42 +98,45 @@ where
|
||||
&mut self.backend
|
||||
}
|
||||
|
||||
/// Builds a string representing the minimal escape sequences and characters set necessary to
|
||||
/// update the UI and writes it to stdout.
|
||||
pub fn flush(&mut self) -> Result<(), io::Error> {
|
||||
let width = self.buffers[self.current].area.width;
|
||||
let content = self.buffers[self.current]
|
||||
.content
|
||||
.iter()
|
||||
.zip(self.buffers[1 - self.current].content.iter())
|
||||
.enumerate()
|
||||
.filter_map(|(i, (c, p))| {
|
||||
if c != p {
|
||||
let i = i as u16;
|
||||
let x = i % width;
|
||||
let y = i / width;
|
||||
Some((x, y, c))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
self.backend.draw(content)
|
||||
/// Obtains a difference between the previous and the current buffer and passes it to the
|
||||
/// current backend for drawing.
|
||||
pub fn flush(&mut self) -> io::Result<()> {
|
||||
let previous_buffer = &self.buffers[1 - self.current];
|
||||
let current_buffer = &self.buffers[self.current];
|
||||
let updates = previous_buffer.diff(current_buffer);
|
||||
self.backend.draw(updates.into_iter())
|
||||
}
|
||||
|
||||
/// Updates the interface so that internal buffers matches the current size of the terminal.
|
||||
/// This leads to a full redraw of the screen.
|
||||
pub fn resize(&mut self, area: Rect) -> Result<(), io::Error> {
|
||||
/// Updates the Terminal so that internal buffers match the requested size. Requested size will
|
||||
/// be saved so the size can remain consistent when rendering.
|
||||
/// This leads to a full clear of the screen.
|
||||
pub fn resize(&mut self, area: Rect) -> io::Result<()> {
|
||||
self.buffers[self.current].resize(area);
|
||||
self.buffers[1 - self.current].reset();
|
||||
self.buffers[1 - self.current].resize(area);
|
||||
self.known_size = area;
|
||||
self.backend.clear()
|
||||
}
|
||||
|
||||
/// Flushes the current internal state and prepares the interface for the next draw call
|
||||
pub fn draw<F>(&mut self, f: F) -> Result<(), io::Error>
|
||||
/// Queries the backend for size and resizes if it doesn't match the previous size.
|
||||
pub fn autoresize(&mut self) -> io::Result<()> {
|
||||
let size = self.size()?;
|
||||
if self.known_size != size {
|
||||
self.resize(size)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
|
||||
/// and prepares for the next draw call.
|
||||
pub fn draw<F>(&mut self, f: F) -> io::Result<()>
|
||||
where
|
||||
F: FnOnce(Frame<B>),
|
||||
{
|
||||
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
|
||||
// and the terminal (if growing), which may OOB.
|
||||
self.autoresize()?;
|
||||
|
||||
f(self.get_frame());
|
||||
|
||||
// Draw to stdout
|
||||
@@ -120,16 +151,27 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||
self.backend.hide_cursor()
|
||||
pub fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.hide_cursor()?;
|
||||
self.hidden_cursor = true;
|
||||
Ok(())
|
||||
}
|
||||
pub fn show_cursor(&mut self) -> Result<(), io::Error> {
|
||||
self.backend.show_cursor()
|
||||
pub fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.show_cursor()?;
|
||||
self.hidden_cursor = false;
|
||||
Ok(())
|
||||
}
|
||||
pub fn clear(&mut self) -> Result<(), io::Error> {
|
||||
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
self.backend.get_cursor()
|
||||
}
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.backend.set_cursor(x, y)
|
||||
}
|
||||
pub fn clear(&mut self) -> io::Result<()> {
|
||||
self.backend.clear()
|
||||
}
|
||||
pub fn size(&self) -> Result<Rect, io::Error> {
|
||||
/// Queries the real size of the backend.
|
||||
pub fn size(&self) -> io::Result<Rect> {
|
||||
self.backend.size()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,17 @@ use std::cmp::{max, min};
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
use style::Style;
|
||||
use symbols::bar;
|
||||
use widgets::{Block, Widget};
|
||||
use crate::buffer::Buffer;
|
||||
use crate::layout::Rect;
|
||||
use crate::style::Style;
|
||||
use crate::symbols::bar;
|
||||
use crate::widgets::{Block, Widget};
|
||||
|
||||
/// Display multiple bars in a single widgets
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// # use tui::widgets::{Block, Borders, BarChart};
|
||||
/// # use tui::style::{Style, Color, Modifier};
|
||||
/// # fn main() {
|
||||
@@ -22,7 +21,7 @@ use widgets::{Block, Widget};
|
||||
/// .bar_width(3)
|
||||
/// .bar_gap(1)
|
||||
/// .style(Style::default().fg(Color::Yellow).bg(Color::Red))
|
||||
/// .value_style(Style::default().fg(Color::Red).modifier(Modifier::Bold))
|
||||
/// .value_style(Style::default().fg(Color::Red).modifier(Modifier::BOLD))
|
||||
/// .label_style(Style::default().fg(Color::White))
|
||||
/// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
|
||||
/// .max(4);
|
||||
@@ -121,7 +120,7 @@ impl<'a> Widget for BarChart<'a> {
|
||||
return;
|
||||
}
|
||||
|
||||
self.background(&chart_area, buf, self.style.bg);
|
||||
self.background(chart_area, buf, self.style.bg);
|
||||
|
||||
let max = self
|
||||
.max
|
||||
@@ -154,8 +153,9 @@ impl<'a> Widget for BarChart<'a> {
|
||||
buf.get_mut(
|
||||
chart_area.left() + i as u16 * (self.bar_width + self.bar_gap) + x,
|
||||
chart_area.top() + j,
|
||||
).set_symbol(symbol)
|
||||
.set_style(self.style);
|
||||
)
|
||||
.set_symbol(symbol)
|
||||
.set_style(self.style);
|
||||
}
|
||||
|
||||
if d.1 > 8 {
|
||||
@@ -177,7 +177,7 @@ impl<'a> Widget for BarChart<'a> {
|
||||
+ (self.bar_width - width) / 2,
|
||||
chart_area.bottom() - 2,
|
||||
value_label,
|
||||
&self.value_style,
|
||||
self.value_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -186,7 +186,7 @@ impl<'a> Widget for BarChart<'a> {
|
||||
chart_area.bottom() - 1,
|
||||
label,
|
||||
self.bar_width as usize,
|
||||
&self.label_style,
|
||||
self.label_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
use style::Style;
|
||||
use symbols::line;
|
||||
use widgets::{Borders, Widget};
|
||||
use crate::buffer::Buffer;
|
||||
use crate::layout::Rect;
|
||||
use crate::style::Style;
|
||||
use crate::symbols::line;
|
||||
use crate::widgets::{Borders, Widget};
|
||||
|
||||
/// Base widget to be used with all upper level ones. It may be used to display a box border around
|
||||
/// the widget and/or add a title.
|
||||
@@ -10,7 +10,6 @@ use widgets::{Borders, Widget};
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// # use tui::widgets::{Block, Borders};
|
||||
/// # use tui::style::{Style, Color};
|
||||
/// # fn main() {
|
||||
@@ -104,7 +103,7 @@ impl<'a> Widget for Block<'a> {
|
||||
return;
|
||||
}
|
||||
|
||||
self.background(&area, buf, self.style.bg);
|
||||
self.background(area, buf, self.style.bg);
|
||||
|
||||
// Sides
|
||||
if self.borders.intersects(Borders::LEFT) {
|
||||
@@ -178,7 +177,7 @@ impl<'a> Widget for Block<'a> {
|
||||
area.top(),
|
||||
title,
|
||||
width as usize,
|
||||
&self.title_style,
|
||||
self.title_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::Shape;
|
||||
use style::Color;
|
||||
use crate::style::Color;
|
||||
|
||||
/// Shape to draw a line from (x1, y1) to (x2, y2) with the given color
|
||||
pub struct Line {
|
||||
@@ -23,6 +23,7 @@ pub struct LineIterator {
|
||||
|
||||
impl Iterator for LineIterator {
|
||||
type Item = (f64, f64);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current < self.end {
|
||||
let pos = (
|
||||
@@ -40,6 +41,7 @@ impl Iterator for LineIterator {
|
||||
impl<'a> IntoIterator for &'a Line {
|
||||
type Item = (f64, f64);
|
||||
type IntoIter = LineIterator;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
let dx = self.x1.max(self.x2) - self.x1.min(self.x2);
|
||||
let dy = self.y1.max(self.y2) - self.y1.min(self.y2);
|
||||
@@ -63,6 +65,7 @@ impl<'a> Shape<'a> for Line {
|
||||
fn color(&self) -> Color {
|
||||
self.color
|
||||
}
|
||||
|
||||
fn points(&'a self) -> Box<Iterator<Item = (f64, f64)> + 'a> {
|
||||
Box::new(self.into_iter())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use style::Color;
|
||||
use widgets::canvas::points::PointsIterator;
|
||||
use widgets::canvas::world::{WORLD_HIGH_RESOLUTION, WORLD_LOW_RESOLUTION};
|
||||
use widgets::canvas::Shape;
|
||||
use crate::style::Color;
|
||||
use crate::widgets::canvas::points::PointsIterator;
|
||||
use crate::widgets::canvas::world::{WORLD_HIGH_RESOLUTION, WORLD_LOW_RESOLUTION};
|
||||
use crate::widgets::canvas::Shape;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum MapResolution {
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
mod line;
|
||||
mod map;
|
||||
mod points;
|
||||
mod rectangle;
|
||||
mod world;
|
||||
|
||||
pub use self::line::Line;
|
||||
pub use self::map::{Map, MapResolution};
|
||||
pub use self::points::Points;
|
||||
pub use self::rectangle::Rectangle;
|
||||
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
use style::{Color, Style};
|
||||
use widgets::{Block, Widget};
|
||||
use crate::buffer::Buffer;
|
||||
use crate::layout::Rect;
|
||||
use crate::style::{Color, Style};
|
||||
use crate::widgets::{Block, Widget};
|
||||
|
||||
pub const DOTS: [[u16; 2]; 4] = [
|
||||
[0x0001, 0x0008],
|
||||
@@ -132,9 +134,9 @@ impl<'a> Context<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// # use tui::widgets::{Block, Borders};
|
||||
/// # use tui::widgets::canvas::{Canvas, Shape, Line, Map, MapResolution};
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use tui::widgets::canvas::{Canvas, Shape, Line, Rectangle, Map, MapResolution};
|
||||
/// # use tui::style::Color;
|
||||
/// # fn main() {
|
||||
/// Canvas::default()
|
||||
@@ -142,23 +144,25 @@ impl<'a> Context<'a> {
|
||||
/// .x_bounds([-180.0, 180.0])
|
||||
/// .y_bounds([-90.0, 90.0])
|
||||
/// .paint(|ctx| {
|
||||
/// ctx.draw(&Map{
|
||||
/// ctx.draw(&Map {
|
||||
/// resolution: MapResolution::High,
|
||||
/// color: Color::White
|
||||
/// });
|
||||
/// ctx.layer();
|
||||
/// ctx.draw(&Line{
|
||||
/// ctx.draw(&Line {
|
||||
/// x1: 0.0,
|
||||
/// y1: 10.0,
|
||||
/// x2: 10.0,
|
||||
/// y2: 10.0,
|
||||
/// color: Color::White,
|
||||
/// });
|
||||
/// ctx.draw(&Line{
|
||||
/// x1: 10.0,
|
||||
/// y1: 10.0,
|
||||
/// x2: 20.0,
|
||||
/// y2: 20.0,
|
||||
/// ctx.draw(&Rectangle {
|
||||
/// rect: Rect {
|
||||
/// x: 10,
|
||||
/// y: 20,
|
||||
/// width: 10,
|
||||
/// height: 10,
|
||||
/// },
|
||||
/// color: Color::Red
|
||||
/// });
|
||||
/// });
|
||||
@@ -285,7 +289,7 @@ where
|
||||
dx + canvas_area.left(),
|
||||
dy + canvas_area.top(),
|
||||
label.text,
|
||||
&style.fg(label.color),
|
||||
style.fg(label.color),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::slice;
|
||||
|
||||
use super::Shape;
|
||||
use style::Color;
|
||||
use crate::style::Color;
|
||||
|
||||
/// A shape to draw a group of points with the given color
|
||||
pub struct Points<'a> {
|
||||
|
||||
54
src/widgets/canvas/rectangle.rs
Normal file
54
src/widgets/canvas/rectangle.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use crate::layout::Rect;
|
||||
use crate::style::Color;
|
||||
use crate::widgets::canvas::{Line, Shape};
|
||||
use itertools::Itertools;
|
||||
|
||||
/// Shape to draw a rectangle from a `Rect` with the given color
|
||||
pub struct Rectangle {
|
||||
pub rect: Rect,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
impl<'a> Shape<'a> for Rectangle {
|
||||
fn color(&self) -> Color {
|
||||
self.color
|
||||
}
|
||||
|
||||
fn points(&'a self) -> Box<Iterator<Item = (f64, f64)> + 'a> {
|
||||
let left_line = Line {
|
||||
x1: f64::from(self.rect.x),
|
||||
y1: f64::from(self.rect.y),
|
||||
x2: f64::from(self.rect.x),
|
||||
y2: f64::from(self.rect.y + self.rect.height),
|
||||
color: self.color,
|
||||
};
|
||||
let top_line = Line {
|
||||
x1: f64::from(self.rect.x),
|
||||
y1: f64::from(self.rect.y + self.rect.height),
|
||||
x2: f64::from(self.rect.x + self.rect.width),
|
||||
y2: f64::from(self.rect.y + self.rect.height),
|
||||
color: self.color,
|
||||
};
|
||||
let right_line = Line {
|
||||
x1: f64::from(self.rect.x + self.rect.width),
|
||||
y1: f64::from(self.rect.y),
|
||||
x2: f64::from(self.rect.x + self.rect.width),
|
||||
y2: f64::from(self.rect.y + self.rect.height),
|
||||
color: self.color,
|
||||
};
|
||||
let bottom_line = Line {
|
||||
x1: f64::from(self.rect.x),
|
||||
y1: f64::from(self.rect.y),
|
||||
x2: f64::from(self.rect.x + self.rect.width),
|
||||
y2: f64::from(self.rect.y),
|
||||
color: self.color,
|
||||
};
|
||||
Box::new(
|
||||
left_line.into_iter().merge(
|
||||
top_line
|
||||
.into_iter()
|
||||
.merge(right_line.into_iter().merge(bottom_line.into_iter())),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,12 @@ use std::cmp::max;
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
use style::Style;
|
||||
use symbols;
|
||||
use widgets::canvas::{Canvas, Points};
|
||||
use widgets::{Block, Borders, Widget};
|
||||
use crate::buffer::Buffer;
|
||||
use crate::layout::Rect;
|
||||
use crate::style::Style;
|
||||
use crate::symbols;
|
||||
use crate::widgets::canvas::{Canvas, Points};
|
||||
use crate::widgets::{Block, Borders, Widget};
|
||||
|
||||
/// An X or Y axis for the chart widget
|
||||
pub struct Axis<'a, L>
|
||||
@@ -166,7 +166,6 @@ impl Default for ChartLayout {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// # use tui::widgets::{Block, Borders, Chart, Axis, Dataset, Marker};
|
||||
/// # use tui::style::{Style, Color};
|
||||
/// # fn main() {
|
||||
@@ -175,13 +174,13 @@ impl Default for ChartLayout {
|
||||
/// .x_axis(Axis::default()
|
||||
/// .title("X Axis")
|
||||
/// .title_style(Style::default().fg(Color::Red))
|
||||
/// .style(Style::default().fg(Color::Gray))
|
||||
/// .style(Style::default().fg(Color::White))
|
||||
/// .bounds([0.0, 10.0])
|
||||
/// .labels(&["0.0", "5.0", "10.0"]))
|
||||
/// .y_axis(Axis::default()
|
||||
/// .title("Y Axis")
|
||||
/// .title_style(Style::default().fg(Color::Red))
|
||||
/// .style(Style::default().fg(Color::Gray))
|
||||
/// .style(Style::default().fg(Color::White))
|
||||
/// .bounds([0.0, 10.0])
|
||||
/// .labels(&["0.0", "5.0", "10.0"]))
|
||||
/// .datasets(&[Dataset::default()
|
||||
@@ -357,16 +356,16 @@ where
|
||||
return;
|
||||
}
|
||||
|
||||
self.background(&chart_area, buf, self.style.bg);
|
||||
self.background(chart_area, buf, self.style.bg);
|
||||
|
||||
if let Some((x, y)) = layout.title_x {
|
||||
let title = self.x_axis.title.unwrap();
|
||||
buf.set_string(x, y, title, &self.x_axis.style);
|
||||
buf.set_string(x, y, title, self.x_axis.style);
|
||||
}
|
||||
|
||||
if let Some((x, y)) = layout.title_y {
|
||||
let title = self.y_axis.title.unwrap();
|
||||
buf.set_string(x, y, title, &self.y_axis.style);
|
||||
buf.set_string(x, y, title, self.y_axis.style);
|
||||
}
|
||||
|
||||
if let Some(y) = layout.label_x {
|
||||
@@ -380,7 +379,7 @@ where
|
||||
- label.as_ref().width() as u16,
|
||||
y,
|
||||
label.as_ref(),
|
||||
&self.x_axis.labels_style,
|
||||
self.x_axis.labels_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -396,7 +395,7 @@ where
|
||||
x,
|
||||
graph_area.bottom() - 1 - dy,
|
||||
label.as_ref(),
|
||||
&self.y_axis.labels_style,
|
||||
self.y_axis.labels_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -428,24 +427,26 @@ where
|
||||
|
||||
for dataset in self.datasets {
|
||||
match dataset.marker {
|
||||
Marker::Dot => for &(x, y) in dataset.data.iter().filter(|&&(x, y)| {
|
||||
!(x < self.x_axis.bounds[0]
|
||||
|| x > self.x_axis.bounds[1]
|
||||
|| y < self.y_axis.bounds[0]
|
||||
|| y > self.y_axis.bounds[1])
|
||||
}) {
|
||||
let dy = ((self.y_axis.bounds[1] - y) * f64::from(graph_area.height - 1)
|
||||
/ (self.y_axis.bounds[1] - self.y_axis.bounds[0]))
|
||||
as u16;
|
||||
let dx = ((x - self.x_axis.bounds[0]) * f64::from(graph_area.width - 1)
|
||||
/ (self.x_axis.bounds[1] - self.x_axis.bounds[0]))
|
||||
as u16;
|
||||
Marker::Dot => {
|
||||
for &(x, y) in dataset.data.iter().filter(|&&(x, y)| {
|
||||
!(x < self.x_axis.bounds[0]
|
||||
|| x > self.x_axis.bounds[1]
|
||||
|| y < self.y_axis.bounds[0]
|
||||
|| y > self.y_axis.bounds[1])
|
||||
}) {
|
||||
let dy = ((self.y_axis.bounds[1] - y) * f64::from(graph_area.height - 1)
|
||||
/ (self.y_axis.bounds[1] - self.y_axis.bounds[0]))
|
||||
as u16;
|
||||
let dx = ((x - self.x_axis.bounds[0]) * f64::from(graph_area.width - 1)
|
||||
/ (self.x_axis.bounds[1] - self.x_axis.bounds[0]))
|
||||
as u16;
|
||||
|
||||
buf.get_mut(graph_area.left() + dx, graph_area.top() + dy)
|
||||
.set_symbol(symbols::DOT)
|
||||
.set_fg(dataset.style.fg)
|
||||
.set_bg(dataset.style.bg);
|
||||
},
|
||||
buf.get_mut(graph_area.left() + dx, graph_area.top() + dy)
|
||||
.set_symbol(symbols::DOT)
|
||||
.set_fg(dataset.style.fg)
|
||||
.set_bg(dataset.style.bg);
|
||||
}
|
||||
}
|
||||
Marker::Braille => {
|
||||
Canvas::default()
|
||||
.background_color(self.style.bg)
|
||||
@@ -471,7 +472,7 @@ where
|
||||
legend_area.x + 1,
|
||||
legend_area.y + 1 + i as u16,
|
||||
dataset.name,
|
||||
&dataset.style,
|
||||
dataset.style,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
use style::{Color, Style};
|
||||
use widgets::{Block, Widget};
|
||||
use crate::buffer::Buffer;
|
||||
use crate::layout::Rect;
|
||||
use crate::style::{Color, Style};
|
||||
use crate::widgets::{Block, Widget};
|
||||
|
||||
/// A widget to display a task progress.
|
||||
///
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// # use tui::widgets::{Widget, Gauge, Block, Borders};
|
||||
/// # use tui::style::{Style, Color, Modifier};
|
||||
/// # fn main() {
|
||||
/// Gauge::default()
|
||||
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
|
||||
/// .style(Style::default().fg(Color::White).bg(Color::Black).modifier(Modifier::Italic))
|
||||
/// .style(Style::default().fg(Color::White).bg(Color::Black).modifier(Modifier::ITALIC))
|
||||
/// .percent(20);
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct Gauge<'a> {
|
||||
block: Option<Block<'a>>,
|
||||
percent: u16,
|
||||
ratio: f64,
|
||||
label: Option<&'a str>,
|
||||
style: Style,
|
||||
}
|
||||
@@ -31,7 +30,7 @@ impl<'a> Default for Gauge<'a> {
|
||||
fn default() -> Gauge<'a> {
|
||||
Gauge {
|
||||
block: None,
|
||||
percent: 0,
|
||||
ratio: 0.0,
|
||||
label: None,
|
||||
style: Default::default(),
|
||||
}
|
||||
@@ -45,7 +44,21 @@ impl<'a> Gauge<'a> {
|
||||
}
|
||||
|
||||
pub fn percent(mut self, percent: u16) -> Gauge<'a> {
|
||||
self.percent = percent;
|
||||
assert!(
|
||||
percent <= 100,
|
||||
"Percentage should be between 0 and 100 inclusively."
|
||||
);
|
||||
self.ratio = f64::from(percent) / 100.0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets ratio ([0.0, 1.0]) directly.
|
||||
pub fn ratio(mut self, ratio: f64) -> Gauge<'a> {
|
||||
assert!(
|
||||
ratio <= 1.0 && ratio >= 0.0,
|
||||
"Ratio should be between 0 and 1 inclusively."
|
||||
);
|
||||
self.ratio = ratio;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -74,11 +87,11 @@ impl<'a> Widget for Gauge<'a> {
|
||||
}
|
||||
|
||||
if self.style.bg != Color::Reset {
|
||||
self.background(&gauge_area, buf, self.style.bg);
|
||||
self.background(gauge_area, buf, self.style.bg);
|
||||
}
|
||||
|
||||
let center = gauge_area.height / 2 + gauge_area.top();
|
||||
let width = (gauge_area.width * self.percent) / 100;
|
||||
let width = (f64::from(gauge_area.width) * self.ratio).round() as u16;
|
||||
let end = gauge_area.left() + width;
|
||||
for y in gauge_area.top()..gauge_area.bottom() {
|
||||
// Gauge
|
||||
@@ -88,11 +101,11 @@ impl<'a> Widget for Gauge<'a> {
|
||||
|
||||
if y == center {
|
||||
// Label
|
||||
let precent_label = format!("{}%", self.percent);
|
||||
let precent_label = format!("{}%", (self.ratio * 100.0).round());
|
||||
let label = self.label.unwrap_or(&precent_label);
|
||||
let label_width = label.width() as u16;
|
||||
let middle = (gauge_area.width - label_width) / 2 + gauge_area.left();
|
||||
buf.set_string(middle, y, label, &self.style);
|
||||
buf.set_string(middle, y, label, self.style);
|
||||
}
|
||||
|
||||
// Fix colors
|
||||
@@ -104,3 +117,26 @@ impl<'a> Widget for Gauge<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn gauge_invalid_percentage() {
|
||||
Gauge::default().percent(110);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn gauge_invalid_ratio_upper_bound() {
|
||||
Gauge::default().ratio(1.1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn gauge_invalid_ratio_lower_bound() {
|
||||
Gauge::default().ratio(-0.5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
use std::fmt::Display;
|
||||
use std::iter;
|
||||
use std::iter::Iterator;
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use buffer::Buffer;
|
||||
use layout::{Corner, Rect};
|
||||
use style::Style;
|
||||
use widgets::{Block, Widget};
|
||||
use crate::buffer::Buffer;
|
||||
use crate::layout::{Corner, Rect};
|
||||
use crate::style::Style;
|
||||
use crate::widgets::{Block, Text, Widget};
|
||||
|
||||
pub enum Item<'i, D: 'i> {
|
||||
Data(D),
|
||||
StyledData(D, &'i Style),
|
||||
}
|
||||
|
||||
pub struct List<'b, 'i, L, D: 'i>
|
||||
pub struct List<'b, L>
|
||||
where
|
||||
L: Iterator<Item = Item<'i, D>>,
|
||||
L: Iterator<Item = Text<'b>>,
|
||||
{
|
||||
block: Option<Block<'b>>,
|
||||
items: L,
|
||||
@@ -24,11 +18,11 @@ where
|
||||
start_corner: Corner,
|
||||
}
|
||||
|
||||
impl<'b, 'i, L, D> Default for List<'b, 'i, L, D>
|
||||
impl<'b, L> Default for List<'b, L>
|
||||
where
|
||||
L: Iterator<Item = Item<'i, D>> + Default,
|
||||
L: Iterator<Item = Text<'b>> + Default,
|
||||
{
|
||||
fn default() -> List<'b, 'i, L, D> {
|
||||
fn default() -> List<'b, L> {
|
||||
List {
|
||||
block: None,
|
||||
items: L::default(),
|
||||
@@ -38,11 +32,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<'b, 'i, L, D> List<'b, 'i, L, D>
|
||||
impl<'b, L> List<'b, L>
|
||||
where
|
||||
L: Iterator<Item = Item<'i, D>>,
|
||||
L: Iterator<Item = Text<'b>>,
|
||||
{
|
||||
pub fn new(items: L) -> List<'b, 'i, L, D> {
|
||||
pub fn new(items: L) -> List<'b, L> {
|
||||
List {
|
||||
block: None,
|
||||
items,
|
||||
@@ -51,34 +45,33 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn block(mut self, block: Block<'b>) -> List<'b, 'i, L, D> {
|
||||
pub fn block(mut self, block: Block<'b>) -> List<'b, L> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn items<I>(mut self, items: I) -> List<'b, 'i, L, D>
|
||||
pub fn items<I>(mut self, items: I) -> List<'b, L>
|
||||
where
|
||||
I: IntoIterator<Item = Item<'i, D>, IntoIter = L>,
|
||||
I: IntoIterator<Item = Text<'b>, IntoIter = L>,
|
||||
{
|
||||
self.items = items.into_iter();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> List<'b, 'i, L, D> {
|
||||
pub fn style(mut self, style: Style) -> List<'b, L> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn start_corner(mut self, corner: Corner) -> List<'b, 'i, L, D> {
|
||||
pub fn start_corner(mut self, corner: Corner) -> List<'b, L> {
|
||||
self.start_corner = corner;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'b, 'i, L, D> Widget for List<'b, 'i, L, D>
|
||||
impl<'b, L> Widget for List<'b, L>
|
||||
where
|
||||
L: Iterator<Item = Item<'i, D>>,
|
||||
D: Display,
|
||||
L: Iterator<Item = Text<'b>>,
|
||||
{
|
||||
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
|
||||
let list_area = match self.block {
|
||||
@@ -93,7 +86,7 @@ where
|
||||
return;
|
||||
}
|
||||
|
||||
self.background(&list_area, buf, self.style.bg);
|
||||
self.background(list_area, buf, self.style.bg);
|
||||
|
||||
for (i, item) in self
|
||||
.items
|
||||
@@ -108,17 +101,11 @@ where
|
||||
_ => (list_area.left(), list_area.top() + i as u16),
|
||||
};
|
||||
match item {
|
||||
Item::Data(ref v) => {
|
||||
buf.set_stringn(
|
||||
x,
|
||||
y,
|
||||
&format!("{}", v),
|
||||
list_area.width as usize,
|
||||
&Style::default(),
|
||||
);
|
||||
Text::Raw(ref v) => {
|
||||
buf.set_stringn(x, y, v, list_area.width as usize, Style::default());
|
||||
}
|
||||
Item::StyledData(ref v, s) => {
|
||||
buf.set_stringn(x, y, &format!("{}", v), list_area.width as usize, s);
|
||||
Text::Styled(ref v, s) => {
|
||||
buf.set_stringn(x, y, v, list_area.width as usize, s);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -130,7 +117,6 @@ where
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// # use tui::widgets::{Block, Borders, SelectableList};
|
||||
/// # use tui::style::{Style, Color, Modifier};
|
||||
/// # fn main() {
|
||||
@@ -139,7 +125,7 @@ where
|
||||
/// .items(&["Item 1", "Item 2", "Item 3"])
|
||||
/// .select(Some(1))
|
||||
/// .style(Style::default().fg(Color::White))
|
||||
/// .highlight_style(Style::default().modifier(Modifier::Italic))
|
||||
/// .highlight_style(Style::default().modifier(Modifier::ITALIC))
|
||||
/// .highlight_symbol(">>");
|
||||
/// # }
|
||||
/// ```
|
||||
@@ -216,8 +202,8 @@ impl<'b> Widget for SelectableList<'b> {
|
||||
|
||||
// Use highlight_style only if something is selected
|
||||
let (selected, highlight_style) = match self.selected {
|
||||
Some(i) => (Some(i), &self.highlight_style),
|
||||
None => (None, &self.style),
|
||||
Some(i) => (Some(i), self.highlight_style),
|
||||
None => (None, self.style),
|
||||
};
|
||||
let highlight_symbol = self.highlight_symbol.unwrap_or("");
|
||||
let blank_symbol = iter::repeat(" ")
|
||||
@@ -239,15 +225,15 @@ impl<'b> Widget for SelectableList<'b> {
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, item)| {
|
||||
.map(|(i, &item)| {
|
||||
if let Some(s) = selected {
|
||||
if i == s {
|
||||
Item::StyledData(format!("{} {}", highlight_symbol, item), highlight_style)
|
||||
Text::styled(format!("{} {}", highlight_symbol, item), highlight_style)
|
||||
} else {
|
||||
Item::StyledData(format!("{} {}", blank_symbol, item), &self.style)
|
||||
Text::styled(format!("{} {}", blank_symbol, item), self.style)
|
||||
}
|
||||
} else {
|
||||
Item::StyledData(item.to_string(), &self.style)
|
||||
Text::styled(item, self.style)
|
||||
}
|
||||
})
|
||||
.skip(offset as usize);
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
use bitflags::bitflags;
|
||||
use std::borrow::Cow;
|
||||
|
||||
mod barchart;
|
||||
mod block;
|
||||
pub mod canvas;
|
||||
@@ -5,6 +8,7 @@ mod chart;
|
||||
mod gauge;
|
||||
mod list;
|
||||
mod paragraph;
|
||||
mod reflow;
|
||||
mod sparkline;
|
||||
mod table;
|
||||
mod tabs;
|
||||
@@ -13,17 +17,17 @@ pub use self::barchart::BarChart;
|
||||
pub use self::block::Block;
|
||||
pub use self::chart::{Axis, Chart, Dataset, Marker};
|
||||
pub use self::gauge::Gauge;
|
||||
pub use self::list::{Item, List, SelectableList};
|
||||
pub use self::paragraph::{Paragraph, Text};
|
||||
pub use self::list::{List, SelectableList};
|
||||
pub use self::paragraph::Paragraph;
|
||||
pub use self::sparkline::Sparkline;
|
||||
pub use self::table::{Row, Table};
|
||||
pub use self::tabs::Tabs;
|
||||
|
||||
use backend::Backend;
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
use style::Color;
|
||||
use terminal::Frame;
|
||||
use crate::backend::Backend;
|
||||
use crate::buffer::Buffer;
|
||||
use crate::layout::Rect;
|
||||
use crate::style::{Color, Style};
|
||||
use crate::terminal::Frame;
|
||||
|
||||
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
|
||||
bitflags! {
|
||||
@@ -43,13 +47,28 @@ bitflags! {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Text<'b> {
|
||||
Raw(Cow<'b, str>),
|
||||
Styled(Cow<'b, str>, Style),
|
||||
}
|
||||
|
||||
impl<'b> Text<'b> {
|
||||
pub fn raw<D: Into<Cow<'b, str>>>(data: D) -> Text<'b> {
|
||||
Text::Raw(data.into())
|
||||
}
|
||||
|
||||
pub fn styled<D: Into<Cow<'b, str>>>(data: D, style: Style) -> Text<'b> {
|
||||
Text::Styled(data.into(), style)
|
||||
}
|
||||
}
|
||||
|
||||
/// Base requirements for a Widget
|
||||
pub trait Widget {
|
||||
/// Draws the current state of the widget in the given buffer. That the only method required to
|
||||
/// implement a custom widget.
|
||||
fn draw(&mut self, area: Rect, buf: &mut Buffer);
|
||||
/// Helper method to quickly set the background of all cells inside the specified area.
|
||||
fn background(&self, area: &Rect, buf: &mut Buffer, color: Color) {
|
||||
fn background(&self, area: Rect, buf: &mut Buffer, color: Color) {
|
||||
for y in area.top()..area.bottom() {
|
||||
for x in area.left()..area.right() {
|
||||
buf.get_mut(x, y).set_bg(color);
|
||||
|
||||
@@ -1,26 +1,33 @@
|
||||
use either::Either;
|
||||
use itertools::{multipeek, MultiPeek};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use buffer::Buffer;
|
||||
use layout::{Alignment, Rect};
|
||||
use style::Style;
|
||||
use widgets::{Block, Widget};
|
||||
use crate::buffer::Buffer;
|
||||
use crate::layout::{Alignment, Rect};
|
||||
use crate::style::Style;
|
||||
use crate::widgets::reflow::{LineComposer, LineTruncator, Styled, WordWrapper};
|
||||
use crate::widgets::{Block, Text, Widget};
|
||||
|
||||
fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 {
|
||||
match alignment {
|
||||
Alignment::Center => (text_area_width / 2).saturating_sub(line_width / 2),
|
||||
Alignment::Right => text_area_width.saturating_sub(line_width),
|
||||
Alignment::Left => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget to display some text.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// # use tui::widgets::{Block, Borders, Paragraph, Text};
|
||||
/// # use tui::style::{Style, Color};
|
||||
/// # use tui::layout::{Alignment};
|
||||
/// # fn main() {
|
||||
/// let text = [
|
||||
/// Text::Data("First line\n"),
|
||||
/// Text::StyledData("Second line\n", Style::default().fg(Color::Red))
|
||||
/// Text::raw("First line\n"),
|
||||
/// Text::styled("Second line\n", Style::default().fg(Color::Red))
|
||||
/// ];
|
||||
/// Paragraph::new(text.iter())
|
||||
/// .block(Block::default().title("Paragraph").borders(Borders::ALL))
|
||||
@@ -49,11 +56,6 @@ where
|
||||
alignment: Alignment,
|
||||
}
|
||||
|
||||
pub enum Text<'b> {
|
||||
Data(&'b str),
|
||||
StyledData(&'b str, Style),
|
||||
}
|
||||
|
||||
impl<'a, 't, T> Paragraph<'a, 't, T>
|
||||
where
|
||||
T: Iterator<Item = &'t Text<'t>>,
|
||||
@@ -101,7 +103,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 't, T> Widget for Paragraph<'a, 't, T>
|
||||
impl<'a, 't, 'b, T> Widget for Paragraph<'a, 't, T>
|
||||
where
|
||||
T: Iterator<Item = &'t Text<'t>>,
|
||||
{
|
||||
@@ -118,89 +120,40 @@ where
|
||||
return;
|
||||
}
|
||||
|
||||
self.background(&text_area, buf, self.style.bg);
|
||||
self.background(text_area, buf, self.style.bg);
|
||||
|
||||
let style = self.style;
|
||||
let styled = self.text.by_ref().flat_map(|t| match *t {
|
||||
Text::Data(d) => {
|
||||
Either::Left(UnicodeSegmentation::graphemes(d, true).map(|g| (g, style)))
|
||||
let mut styled = self.text.by_ref().flat_map(|t| match *t {
|
||||
Text::Raw(ref d) => {
|
||||
let data: &'t str = d; // coerce to &str
|
||||
Either::Left(UnicodeSegmentation::graphemes(data, true).map(|g| Styled(g, style)))
|
||||
}
|
||||
Text::StyledData(d, s) => {
|
||||
Either::Right(UnicodeSegmentation::graphemes(d, true).map(move |g| (g, s)))
|
||||
Text::Styled(ref d, s) => {
|
||||
let data: &'t str = d; // coerce to &str
|
||||
Either::Right(UnicodeSegmentation::graphemes(data, true).map(move |g| Styled(g, s)))
|
||||
}
|
||||
});
|
||||
let mut styled = multipeek(styled);
|
||||
|
||||
fn get_cur_line_len<'a, I: Iterator<Item = (&'a str, Style)>>(
|
||||
styled: &mut MultiPeek<I>,
|
||||
) -> u16 {
|
||||
let mut line_len = 0;
|
||||
while match &styled.peek() {
|
||||
Some(&(x, _)) => x != "\n",
|
||||
None => false,
|
||||
} {
|
||||
line_len += 1;
|
||||
}
|
||||
line_len
|
||||
};
|
||||
|
||||
let mut x = match self.alignment {
|
||||
Alignment::Center => {
|
||||
(text_area.width / 2).saturating_sub(get_cur_line_len(&mut styled) / 2)
|
||||
}
|
||||
Alignment::Right => (text_area.width).saturating_sub(get_cur_line_len(&mut styled)),
|
||||
Alignment::Left => 0,
|
||||
let mut line_composer: Box<dyn LineComposer> = if self.wrapping {
|
||||
Box::new(WordWrapper::new(&mut styled, text_area.width))
|
||||
} else {
|
||||
Box::new(LineTruncator::new(&mut styled, text_area.width))
|
||||
};
|
||||
let mut y = 0;
|
||||
|
||||
let mut remove_leading_whitespaces = false;
|
||||
while let Some((string, style)) = styled.next() {
|
||||
if string == "\n" {
|
||||
x = match self.alignment {
|
||||
Alignment::Center => {
|
||||
(text_area.width / 2).saturating_sub(get_cur_line_len(&mut styled) / 2)
|
||||
}
|
||||
|
||||
Alignment::Right => {
|
||||
(text_area.width).saturating_sub(get_cur_line_len(&mut styled))
|
||||
}
|
||||
Alignment::Left => 0,
|
||||
};
|
||||
y += 1;
|
||||
continue;
|
||||
while let Some((current_line, current_line_width)) = line_composer.next_line() {
|
||||
if y >= self.scroll {
|
||||
let mut x = get_line_offset(current_line_width, text_area.width, self.alignment);
|
||||
for Styled(symbol, style) in current_line {
|
||||
buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll)
|
||||
.set_symbol(symbol)
|
||||
.set_style(*style);
|
||||
x += symbol.width() as u16;
|
||||
}
|
||||
}
|
||||
if x >= text_area.width && self.wrapping {
|
||||
x = match self.alignment {
|
||||
Alignment::Center => {
|
||||
(text_area.width / 2).saturating_sub(get_cur_line_len(&mut styled) / 2)
|
||||
}
|
||||
|
||||
Alignment::Right => {
|
||||
(text_area.width).saturating_sub(get_cur_line_len(&mut styled) + 1)
|
||||
}
|
||||
Alignment::Left => 0,
|
||||
};
|
||||
y += 1;
|
||||
remove_leading_whitespaces = true
|
||||
}
|
||||
|
||||
if remove_leading_whitespaces && string == " " {
|
||||
continue;
|
||||
}
|
||||
remove_leading_whitespaces = false;
|
||||
|
||||
if y > text_area.height + self.scroll - 1 {
|
||||
y += 1;
|
||||
if y >= text_area.height + self.scroll {
|
||||
break;
|
||||
}
|
||||
|
||||
if y < self.scroll {
|
||||
continue;
|
||||
}
|
||||
|
||||
buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll)
|
||||
.set_symbol(string)
|
||||
.set_style(style);
|
||||
x += string.width() as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
446
src/widgets/reflow.rs
Normal file
446
src/widgets/reflow.rs
Normal file
@@ -0,0 +1,446 @@
|
||||
use crate::style::Style;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
const NBSP: &str = "\u{00a0}";
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Styled<'a>(pub &'a str, pub Style);
|
||||
|
||||
/// A state machine to pack styled symbols into lines.
|
||||
/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming
|
||||
/// iterators for that).
|
||||
pub trait LineComposer<'a> {
|
||||
fn next_line(&mut self) -> Option<(&[Styled<'a>], u16)>;
|
||||
}
|
||||
|
||||
/// A state machine that wraps lines on word boundaries.
|
||||
pub struct WordWrapper<'a, 'b> {
|
||||
symbols: &'b mut Iterator<Item = Styled<'a>>,
|
||||
max_line_width: u16,
|
||||
current_line: Vec<Styled<'a>>,
|
||||
next_line: Vec<Styled<'a>>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> WordWrapper<'a, 'b> {
|
||||
pub fn new(
|
||||
symbols: &'b mut Iterator<Item = Styled<'a>>,
|
||||
max_line_width: u16,
|
||||
) -> WordWrapper<'a, 'b> {
|
||||
WordWrapper {
|
||||
symbols,
|
||||
max_line_width,
|
||||
current_line: vec![],
|
||||
next_line: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
|
||||
fn next_line(&mut self) -> Option<(&[Styled<'a>], u16)> {
|
||||
if self.max_line_width == 0 {
|
||||
return None;
|
||||
}
|
||||
std::mem::swap(&mut self.current_line, &mut self.next_line);
|
||||
self.next_line.truncate(0);
|
||||
|
||||
let mut current_line_width = self
|
||||
.current_line
|
||||
.iter()
|
||||
.map(|Styled(c, _)| c.width() as u16)
|
||||
.sum();
|
||||
|
||||
let mut symbols_to_last_word_end: usize = 0;
|
||||
let mut width_to_last_word_end: u16 = 0;
|
||||
let mut prev_whitespace = false;
|
||||
let mut symbols_exhausted = true;
|
||||
for Styled(symbol, style) in &mut self.symbols {
|
||||
symbols_exhausted = false;
|
||||
let symbol_whitespace = symbol.chars().all(&char::is_whitespace);
|
||||
|
||||
// Ignore characters wider that the total max width.
|
||||
if symbol.width() as u16 > self.max_line_width
|
||||
// Skip leading whitespace.
|
||||
|| symbol_whitespace && symbol != "\n" && current_line_width == 0
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Break on newline and discard it.
|
||||
if symbol == "\n" {
|
||||
if prev_whitespace {
|
||||
current_line_width = width_to_last_word_end;
|
||||
self.current_line.truncate(symbols_to_last_word_end);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Mark the previous symbol as word end.
|
||||
if symbol_whitespace && !prev_whitespace && symbol != NBSP {
|
||||
symbols_to_last_word_end = self.current_line.len();
|
||||
width_to_last_word_end = current_line_width;
|
||||
}
|
||||
|
||||
self.current_line.push(Styled(symbol, style));
|
||||
current_line_width += symbol.width() as u16;
|
||||
|
||||
if current_line_width > self.max_line_width {
|
||||
// If there was no word break in the text, wrap at the end of the line.
|
||||
let (truncate_at, truncated_width) = if symbols_to_last_word_end != 0 {
|
||||
(symbols_to_last_word_end, width_to_last_word_end)
|
||||
} else {
|
||||
(self.current_line.len() - 1, self.max_line_width)
|
||||
};
|
||||
|
||||
// Push the remainder to the next line but strip leading whitespace:
|
||||
{
|
||||
let remainder = &self.current_line[truncate_at..];
|
||||
if let Some(remainder_nonwhite) = remainder
|
||||
.iter()
|
||||
.position(|Styled(c, _)| !c.chars().all(&char::is_whitespace))
|
||||
{
|
||||
self.next_line
|
||||
.extend_from_slice(&remainder[remainder_nonwhite..]);
|
||||
}
|
||||
}
|
||||
self.current_line.truncate(truncate_at);
|
||||
current_line_width = truncated_width;
|
||||
break;
|
||||
}
|
||||
|
||||
prev_whitespace = symbol_whitespace;
|
||||
}
|
||||
|
||||
// Even if the iterator is exhausted, pass the previous remainder.
|
||||
if symbols_exhausted && self.current_line.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((&self.current_line[..], current_line_width))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A state machine that truncates overhanging lines.
|
||||
pub struct LineTruncator<'a, 'b> {
|
||||
symbols: &'b mut Iterator<Item = Styled<'a>>,
|
||||
max_line_width: u16,
|
||||
current_line: Vec<Styled<'a>>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> LineTruncator<'a, 'b> {
|
||||
pub fn new(
|
||||
symbols: &'b mut Iterator<Item = Styled<'a>>,
|
||||
max_line_width: u16,
|
||||
) -> LineTruncator<'a, 'b> {
|
||||
LineTruncator {
|
||||
symbols,
|
||||
max_line_width,
|
||||
current_line: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
|
||||
fn next_line(&mut self) -> Option<(&[Styled<'a>], u16)> {
|
||||
if self.max_line_width == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.current_line.truncate(0);
|
||||
let mut current_line_width = 0;
|
||||
|
||||
let mut skip_rest = false;
|
||||
let mut symbols_exhausted = true;
|
||||
for Styled(symbol, style) in &mut self.symbols {
|
||||
symbols_exhausted = false;
|
||||
|
||||
// Ignore characters wider that the total max width.
|
||||
if symbol.width() as u16 > self.max_line_width {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Break on newline and discard it.
|
||||
if symbol == "\n" {
|
||||
break;
|
||||
}
|
||||
|
||||
if current_line_width + symbol.width() as u16 > self.max_line_width {
|
||||
// Exhaust the remainder of the line.
|
||||
skip_rest = true;
|
||||
break;
|
||||
}
|
||||
|
||||
current_line_width += symbol.width() as u16;
|
||||
self.current_line.push(Styled(symbol, style));
|
||||
}
|
||||
|
||||
if skip_rest {
|
||||
for Styled(symbol, _) in &mut self.symbols {
|
||||
if symbol == "\n" {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if symbols_exhausted && self.current_line.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((&self.current_line[..], current_line_width))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
enum Composer {
|
||||
WordWrapper,
|
||||
LineTruncator,
|
||||
}
|
||||
|
||||
fn run_composer(which: Composer, text: &str, text_area_width: u16) -> (Vec<String>, Vec<u16>) {
|
||||
let style = Default::default();
|
||||
let mut styled = UnicodeSegmentation::graphemes(text, true).map(|g| Styled(g, style));
|
||||
let mut composer: Box<dyn LineComposer> = match which {
|
||||
Composer::WordWrapper => Box::new(WordWrapper::new(&mut styled, text_area_width)),
|
||||
Composer::LineTruncator => Box::new(LineTruncator::new(&mut styled, text_area_width)),
|
||||
};
|
||||
let mut lines = vec![];
|
||||
let mut widths = vec![];
|
||||
while let Some((styled, width)) = composer.next_line() {
|
||||
let line = styled
|
||||
.iter()
|
||||
.map(|Styled(g, _style)| *g)
|
||||
.collect::<String>();
|
||||
assert!(width <= text_area_width);
|
||||
lines.push(line);
|
||||
widths.push(width);
|
||||
}
|
||||
(lines, widths)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_one_line() {
|
||||
let width = 40;
|
||||
for i in 1..width {
|
||||
let text = "a".repeat(i);
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper, &text, width as u16);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width as u16);
|
||||
let expected = vec![text];
|
||||
assert_eq!(word_wrapper, expected);
|
||||
assert_eq!(line_truncator, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_short_lines() {
|
||||
let width = 20;
|
||||
let text =
|
||||
"abcdefg\nhijklmno\npabcdefg\nhijklmn\nopabcdefghijk\nlmnopabcd\n\n\nefghijklmno";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
|
||||
let wrapped: Vec<&str> = text.split('\n').collect();
|
||||
assert_eq!(word_wrapper, wrapped);
|
||||
assert_eq!(line_truncator, wrapped);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_long_word() {
|
||||
let width = 20;
|
||||
let text = "abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmno";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width as u16);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
|
||||
|
||||
let wrapped = vec![
|
||||
&text[..width],
|
||||
&text[width..width * 2],
|
||||
&text[width * 2..width * 3],
|
||||
&text[width * 3..],
|
||||
];
|
||||
assert_eq!(
|
||||
word_wrapper, wrapped,
|
||||
"WordWrapper should deect the line cannot be broken on word boundary and \
|
||||
break it at line width limit."
|
||||
);
|
||||
assert_eq!(line_truncator, vec![&text[..width]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_long_sentence() {
|
||||
let width = 20;
|
||||
let text =
|
||||
"abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l m n o";
|
||||
let text_multi_space =
|
||||
"abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l \
|
||||
m n o";
|
||||
let (word_wrapper_single_space, _) =
|
||||
run_composer(Composer::WordWrapper, text, width as u16);
|
||||
let (word_wrapper_multi_space, _) =
|
||||
run_composer(Composer::WordWrapper, text_multi_space, width as u16);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
|
||||
|
||||
let word_wrapped = vec![
|
||||
"abcd efghij",
|
||||
"klmnopabcd efgh",
|
||||
"ijklmnopabcdefg",
|
||||
"hijkl mnopab c d e f",
|
||||
"g h i j k l m n o",
|
||||
];
|
||||
assert_eq!(word_wrapper_single_space, word_wrapped);
|
||||
assert_eq!(word_wrapper_multi_space, word_wrapped);
|
||||
|
||||
assert_eq!(line_truncator, vec![&text[..width]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_zero_width() {
|
||||
let width = 0;
|
||||
let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
|
||||
let expected: Vec<&str> = Vec::new();
|
||||
assert_eq!(word_wrapper, expected);
|
||||
assert_eq!(line_truncator, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_max_line_width_of_1() {
|
||||
let width = 1;
|
||||
let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
|
||||
let expected: Vec<&str> = UnicodeSegmentation::graphemes(text, true)
|
||||
.filter(|g| g.chars().any(|c| !c.is_whitespace()))
|
||||
.collect();
|
||||
assert_eq!(word_wrapper, expected);
|
||||
assert_eq!(line_truncator, vec!["a"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_max_line_width_of_1_double_width_characters() {
|
||||
let width = 1;
|
||||
let text = "コンピュータ上で文字を扱う場合、典型的には文字\naaaによる通信を行う場合にその\
|
||||
両端点では、";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(word_wrapper, vec!["", "a", "a", "a"]);
|
||||
assert_eq!(line_truncator, vec!["", "a"]);
|
||||
}
|
||||
|
||||
/// Tests WordWrapper with words some of which exceed line length and some not.
|
||||
#[test]
|
||||
fn line_composer_word_wrapper_mixed_length() {
|
||||
let width = 20;
|
||||
let text = "abcd efghij klmnopabcdefghijklmnopabcdefghijkl mnopab cdefghi j klmno";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec![
|
||||
"abcd efghij",
|
||||
"klmnopabcdefghijklmn",
|
||||
"opabcdefghijkl",
|
||||
"mnopab cdefghi j",
|
||||
"klmno",
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_double_width_chars() {
|
||||
let width = 20;
|
||||
let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\
|
||||
では、";
|
||||
let (word_wrapper, word_wrapper_width) = run_composer(Composer::WordWrapper, &text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width);
|
||||
assert_eq!(line_truncator, vec!["コンピュータ上で文字"]);
|
||||
let wrapped = vec![
|
||||
"コンピュータ上で文字",
|
||||
"を扱う場合、典型的に",
|
||||
"は文字による通信を行",
|
||||
"う場合にその両端点で",
|
||||
"は、",
|
||||
];
|
||||
assert_eq!(word_wrapper, wrapped);
|
||||
assert_eq!(word_wrapper_width, vec![width, width, width, width, 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_leading_whitespace_removal() {
|
||||
let width = 20;
|
||||
let text = "AAAAAAAAAAAAAAAAAAAA AAA";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", "AAA",]);
|
||||
assert_eq!(line_truncator, vec!["AAAAAAAAAAAAAAAAAAAA"]);
|
||||
}
|
||||
|
||||
/// Tests truncation of leading whitespace.
|
||||
#[test]
|
||||
fn line_composer_lots_of_spaces() {
|
||||
let width = 20;
|
||||
let text = " ";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(word_wrapper, vec![""]);
|
||||
assert_eq!(line_truncator, vec![" "]);
|
||||
}
|
||||
|
||||
/// Tests an input starting with a letter, folowed by spaces - some of the behaviour is
|
||||
/// incidental.
|
||||
#[test]
|
||||
fn line_composer_char_plus_lots_of_spaces() {
|
||||
let width = 20;
|
||||
let text = "a ";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
// What's happening below is: the first line gets consumed, trailing spaces discarded,
|
||||
// after 20 of which a word break occurs (probably shouldn't). The second line break
|
||||
// discards all whitespace. The result should probably be vec!["a"] but it doesn't matter
|
||||
// that much.
|
||||
assert_eq!(word_wrapper, vec!["a", ""]);
|
||||
assert_eq!(line_truncator, vec!["a "]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_composer_word_wrapper_double_width_chars_mixed_with_spaces() {
|
||||
let width = 20;
|
||||
// Japanese seems not to use spaces but we should break on spaces anyway... We're using it
|
||||
// to test double-width chars.
|
||||
// You are more than welcome to add word boundary detection based of alterations of
|
||||
// hiragana and katakana...
|
||||
// This happens to also be a test case for mixed width because regular spaces are single width.
|
||||
let text = "コンピュ ータ上で文字を扱う場合、 典型的には文 字による 通信を行 う場合にその両端点では、";
|
||||
let (word_wrapper, word_wrapper_width) = run_composer(Composer::WordWrapper, text, width);
|
||||
assert_eq!(
|
||||
word_wrapper,
|
||||
vec![
|
||||
"コンピュ",
|
||||
"ータ上で文字を扱う場",
|
||||
"合、 典型的には文",
|
||||
"字による 通信を行",
|
||||
"う場合にその両端点で",
|
||||
"は、",
|
||||
]
|
||||
);
|
||||
// Odd-sized lines have a space in them.
|
||||
assert_eq!(word_wrapper_width, vec![8, 20, 17, 17, 20, 4]);
|
||||
}
|
||||
|
||||
/// Ensure words separated by nbsp are wrapped as if they were a single one.
|
||||
#[test]
|
||||
fn line_composer_word_wrapper_nbsp() {
|
||||
let width = 20;
|
||||
let text = "AAAAAAAAAAAAAAA AAAA\u{00a0}AAA";
|
||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper, text, width);
|
||||
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAA", "AAAA\u{00a0}AAA",]);
|
||||
|
||||
// Ensure that if the character was a regular space, it would be wrapped differently.
|
||||
let text_space = text.replace("\u{00a0}", " ");
|
||||
let (word_wrapper_space, _) = run_composer(Composer::WordWrapper, &text_space, width);
|
||||
assert_eq!(word_wrapper_space, vec!["AAAAAAAAAAAAAAA AAAA", "AAA",]);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,16 @@
|
||||
use std::cmp::min;
|
||||
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
use style::Style;
|
||||
use symbols::bar;
|
||||
use widgets::{Block, Widget};
|
||||
use crate::buffer::Buffer;
|
||||
use crate::layout::Rect;
|
||||
use crate::style::Style;
|
||||
use crate::symbols::bar;
|
||||
use crate::widgets::{Block, Widget};
|
||||
|
||||
/// Widget to render a sparkline over one or more lines.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// # use tui::widgets::{Block, Borders, Sparkline};
|
||||
/// # use tui::style::{Style, Color};
|
||||
/// # fn main() {
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
use std::fmt::Display;
|
||||
use std::iter::Iterator;
|
||||
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
use style::Style;
|
||||
use widgets::{Block, Widget};
|
||||
use crate::buffer::Buffer;
|
||||
use crate::layout::Rect;
|
||||
use crate::style::Style;
|
||||
use crate::widgets::{Block, Widget};
|
||||
|
||||
/// Holds data to be displayed in a Table widget
|
||||
pub enum Row<'i, D, I>
|
||||
pub enum Row<D, I>
|
||||
where
|
||||
D: Iterator<Item = I>,
|
||||
I: Display,
|
||||
{
|
||||
Data(D),
|
||||
StyledData(D, &'i Style),
|
||||
StyledData(D, Style),
|
||||
}
|
||||
|
||||
/// A widget to display data in formatted columns
|
||||
@@ -28,9 +28,9 @@ where
|
||||
/// Table::new(
|
||||
/// ["Col1", "Col2", "Col3"].into_iter(),
|
||||
/// vec![
|
||||
/// Row::StyledData(["Row11", "Row12", "Row13"].into_iter(), &row_style),
|
||||
/// Row::StyledData(["Row21", "Row22", "Row23"].into_iter(), &row_style),
|
||||
/// Row::StyledData(["Row31", "Row32", "Row33"].into_iter(), &row_style),
|
||||
/// Row::StyledData(["Row11", "Row12", "Row13"].into_iter(), row_style),
|
||||
/// Row::StyledData(["Row21", "Row22", "Row23"].into_iter(), row_style),
|
||||
/// Row::StyledData(["Row31", "Row32", "Row33"].into_iter(), row_style),
|
||||
/// Row::Data(["Row41", "Row42", "Row43"].into_iter())
|
||||
/// ].into_iter()
|
||||
/// )
|
||||
@@ -41,13 +41,13 @@ where
|
||||
/// .column_spacing(1);
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct Table<'a, 'i, T, H, I, D, R>
|
||||
pub struct Table<'a, T, H, I, D, R>
|
||||
where
|
||||
T: Display,
|
||||
H: Iterator<Item = T>,
|
||||
I: Display,
|
||||
D: Iterator<Item = I>,
|
||||
R: Iterator<Item = Row<'i, D, I>>,
|
||||
R: Iterator<Item = Row<D, I>>,
|
||||
{
|
||||
/// A block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
@@ -66,15 +66,15 @@ where
|
||||
rows: R,
|
||||
}
|
||||
|
||||
impl<'a, 'i, T, H, I, D, R> Default for Table<'a, 'i, T, H, I, D, R>
|
||||
impl<'a, T, H, I, D, R> Default for Table<'a, T, H, I, D, R>
|
||||
where
|
||||
T: Display,
|
||||
H: Iterator<Item = T> + Default,
|
||||
I: Display,
|
||||
D: Iterator<Item = I>,
|
||||
R: Iterator<Item = Row<'i, D, I>> + Default,
|
||||
R: Iterator<Item = Row<D, I>> + Default,
|
||||
{
|
||||
fn default() -> Table<'a, 'i, T, H, I, D, R> {
|
||||
fn default() -> Table<'a, T, H, I, D, R> {
|
||||
Table {
|
||||
block: None,
|
||||
style: Style::default(),
|
||||
@@ -87,15 +87,15 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'i, T, H, I, D, R> Table<'a, 'i, T, H, I, D, R>
|
||||
impl<'a, T, H, I, D, R> Table<'a, T, H, I, D, R>
|
||||
where
|
||||
T: Display,
|
||||
H: Iterator<Item = T>,
|
||||
I: Display,
|
||||
D: Iterator<Item = I>,
|
||||
R: Iterator<Item = Row<'i, D, I>>,
|
||||
R: Iterator<Item = Row<D, I>>,
|
||||
{
|
||||
pub fn new(header: H, rows: R) -> Table<'a, 'i, T, H, I, D, R> {
|
||||
pub fn new(header: H, rows: R) -> Table<'a, T, H, I, D, R> {
|
||||
Table {
|
||||
block: None,
|
||||
style: Style::default(),
|
||||
@@ -106,12 +106,12 @@ where
|
||||
column_spacing: 1,
|
||||
}
|
||||
}
|
||||
pub fn block(mut self, block: Block<'a>) -> Table<'a, 'i, T, H, I, D, R> {
|
||||
pub fn block(mut self, block: Block<'a>) -> Table<'a, T, H, I, D, R> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header<II>(mut self, header: II) -> Table<'a, 'i, T, H, I, D, R>
|
||||
pub fn header<II>(mut self, header: II) -> Table<'a, T, H, I, D, R>
|
||||
where
|
||||
II: IntoIterator<Item = T, IntoIter = H>,
|
||||
{
|
||||
@@ -119,42 +119,42 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header_style(mut self, style: Style) -> Table<'a, 'i, T, H, I, D, R> {
|
||||
pub fn header_style(mut self, style: Style) -> Table<'a, T, H, I, D, R> {
|
||||
self.header_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn widths(mut self, widths: &'a [u16]) -> Table<'a, 'i, T, H, I, D, R> {
|
||||
pub fn widths(mut self, widths: &'a [u16]) -> Table<'a, T, H, I, D, R> {
|
||||
self.widths = widths;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn rows<II>(mut self, rows: II) -> Table<'a, 'i, T, H, I, D, R>
|
||||
pub fn rows<II>(mut self, rows: II) -> Table<'a, T, H, I, D, R>
|
||||
where
|
||||
II: IntoIterator<Item = Row<'i, D, I>, IntoIter = R>,
|
||||
II: IntoIterator<Item = Row<D, I>, IntoIter = R>,
|
||||
{
|
||||
self.rows = rows.into_iter();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Table<'a, 'i, T, H, I, D, R> {
|
||||
pub fn style(mut self, style: Style) -> Table<'a, T, H, I, D, R> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn column_spacing(mut self, spacing: u16) -> Table<'a, 'i, T, H, I, D, R> {
|
||||
pub fn column_spacing(mut self, spacing: u16) -> Table<'a, T, H, I, D, R> {
|
||||
self.column_spacing = spacing;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'i, T, H, I, D, R> Widget for Table<'a, 'i, T, H, I, D, R>
|
||||
impl<'a, T, H, I, D, R> Widget for Table<'a, T, H, I, D, R>
|
||||
where
|
||||
T: Display,
|
||||
H: Iterator<Item = T>,
|
||||
I: Display,
|
||||
D: Iterator<Item = I>,
|
||||
R: Iterator<Item = Row<'i, D, I>>,
|
||||
R: Iterator<Item = Row<D, I>>,
|
||||
{
|
||||
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
|
||||
// Render block if necessary and get the drawing area
|
||||
@@ -167,7 +167,7 @@ where
|
||||
};
|
||||
|
||||
// Set the background
|
||||
self.background(&table_area, buf, self.style.bg);
|
||||
self.background(table_area, buf, self.style.bg);
|
||||
|
||||
// Save widths of the columns that will fit in the given area
|
||||
let mut x = 0;
|
||||
@@ -185,7 +185,7 @@ where
|
||||
if y < table_area.bottom() {
|
||||
x = table_area.left();
|
||||
for (w, t) in widths.iter().zip(self.header.by_ref()) {
|
||||
buf.set_string(x, y, &format!("{}", t), &self.header_style);
|
||||
buf.set_string(x, y, format!("{}", t), self.header_style);
|
||||
x += *w + self.column_spacing;
|
||||
}
|
||||
}
|
||||
@@ -197,12 +197,12 @@ where
|
||||
let remaining = (table_area.bottom() - y) as usize;
|
||||
for (i, row) in self.rows.by_ref().take(remaining).enumerate() {
|
||||
let (data, style) = match row {
|
||||
Row::Data(d) => (d, &default_style),
|
||||
Row::Data(d) => (d, default_style),
|
||||
Row::StyledData(d, s) => (d, s),
|
||||
};
|
||||
x = table_area.left();
|
||||
for (w, elt) in widths.iter().zip(data) {
|
||||
buf.set_stringn(x, y + i as u16, &format!("{}", elt), *w as usize, style);
|
||||
buf.set_stringn(x, y + i as u16, format!("{}", elt), *w as usize, style);
|
||||
x += *w + self.column_spacing;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
use style::Style;
|
||||
use symbols::line;
|
||||
use widgets::{Block, Widget};
|
||||
use crate::buffer::Buffer;
|
||||
use crate::layout::Rect;
|
||||
use crate::style::Style;
|
||||
use crate::symbols::line;
|
||||
use crate::widgets::{Block, Widget};
|
||||
|
||||
/// A widget to display available tabs in a multiple panels context.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// # use tui::widgets::{Block, Borders, Tabs};
|
||||
/// # use tui::style::{Style, Color};
|
||||
/// # use tui::symbols::{DOT};
|
||||
/// # fn main() {
|
||||
/// Tabs::default()
|
||||
/// .block(Block::default().title("Tabs").borders(Borders::ALL))
|
||||
/// .titles(&["Tab1", "Tab2", "Tab3", "Tab4"])
|
||||
/// .style(Style::default().fg(Color::White))
|
||||
/// .highlight_style(Style::default().fg(Color::Yellow));
|
||||
/// .highlight_style(Style::default().fg(Color::Yellow))
|
||||
/// .divider(DOT);
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct Tabs<'a, T>
|
||||
@@ -36,6 +37,8 @@ where
|
||||
style: Style,
|
||||
/// The style used to display the selected item
|
||||
highlight_style: Style,
|
||||
/// Tab divider
|
||||
divider: &'a str,
|
||||
}
|
||||
|
||||
impl<'a, T> Default for Tabs<'a, T>
|
||||
@@ -49,6 +52,7 @@ where
|
||||
selected: 0,
|
||||
style: Default::default(),
|
||||
highlight_style: Default::default(),
|
||||
divider: line::VERTICAL,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,6 +85,11 @@ where
|
||||
self.highlight_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn divider(mut self, divider: &'a str) -> Tabs<'a, T> {
|
||||
self.divider = divider;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Widget for Tabs<'a, T>
|
||||
@@ -100,14 +109,17 @@ where
|
||||
return;
|
||||
}
|
||||
|
||||
self.background(&tabs_area, buf, self.style.bg);
|
||||
self.background(tabs_area, buf, self.style.bg);
|
||||
|
||||
let mut x = tabs_area.left();
|
||||
for (title, style) in self.titles.iter().enumerate().map(|(i, t)| {
|
||||
let titles_length = self.titles.len();
|
||||
let divider_width = self.divider.width() as u16;
|
||||
for (title, style, last_title) in self.titles.iter().enumerate().map(|(i, t)| {
|
||||
let lt = i + 1 == titles_length;
|
||||
if i == self.selected {
|
||||
(t, &self.highlight_style)
|
||||
(t, self.highlight_style, lt)
|
||||
} else {
|
||||
(t, &self.style)
|
||||
(t, self.style, lt)
|
||||
}
|
||||
}) {
|
||||
x += 1;
|
||||
@@ -116,14 +128,11 @@ where
|
||||
} else {
|
||||
buf.set_string(x, tabs_area.top(), title.as_ref(), style);
|
||||
x += title.as_ref().width() as u16 + 1;
|
||||
if x >= tabs_area.right() {
|
||||
if x >= tabs_area.right() || last_title {
|
||||
break;
|
||||
} else {
|
||||
buf.get_mut(x, tabs_area.top())
|
||||
.set_symbol(line::VERTICAL)
|
||||
.set_fg(self.style.fg)
|
||||
.set_bg(self.style.bg);
|
||||
x += 1;
|
||||
buf.set_string(x, tabs_area.top(), self.divider, style);
|
||||
x += divider_width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
45
tests/block.rs
Normal file
45
tests/block.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use tui::backend::TestBackend;
|
||||
use tui::buffer::Buffer;
|
||||
use tui::layout::Rect;
|
||||
use tui::style::{Color, Style};
|
||||
use tui::widgets::{Block, Borders, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
#[test]
|
||||
fn it_draws_a_block() {
|
||||
let backend = TestBackend::new(10, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|mut f| {
|
||||
Block::default()
|
||||
.title("Title")
|
||||
.borders(Borders::ALL)
|
||||
.title_style(Style::default().fg(Color::LightBlue))
|
||||
.render(
|
||||
&mut f,
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 8,
|
||||
height: 8,
|
||||
},
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"┌Title─┐ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"└──────┘ ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
for x in 1..=5 {
|
||||
expected.get_mut(x, 0).set_fg(Color::LightBlue);
|
||||
}
|
||||
assert_eq!(&expected, terminal.backend().buffer());
|
||||
}
|
||||
42
tests/gauge.rs
Normal file
42
tests/gauge.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use tui::backend::TestBackend;
|
||||
use tui::buffer::Buffer;
|
||||
use tui::layout::{Constraint, Direction, Layout};
|
||||
use tui::widgets::{Block, Borders, Gauge, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
#[test]
|
||||
fn gauge_render() {
|
||||
let backend = TestBackend::new(40, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
Gauge::default()
|
||||
.block(Block::default().title("Percentage").borders(Borders::ALL))
|
||||
.percent(43)
|
||||
.render(&mut f, chunks[0]);
|
||||
Gauge::default()
|
||||
.block(Block::default().title("Ratio").borders(Borders::ALL))
|
||||
.ratio(0.2113139343131)
|
||||
.render(&mut f, chunks[1]);
|
||||
})
|
||||
.unwrap();
|
||||
let expected = Buffer::with_lines(vec![
|
||||
" ",
|
||||
" ",
|
||||
" ┌Percentage────────────────────────┐ ",
|
||||
" │ 43% │ ",
|
||||
" └──────────────────────────────────┘ ",
|
||||
" ┌Ratio─────────────────────────────┐ ",
|
||||
" │ 21% │ ",
|
||||
" └──────────────────────────────────┘ ",
|
||||
" ",
|
||||
" ",
|
||||
]);
|
||||
assert_eq!(&expected, terminal.backend().buffer());
|
||||
}
|
||||
140
tests/paragraph.rs
Normal file
140
tests/paragraph.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
use tui::backend::TestBackend;
|
||||
use tui::buffer::Buffer;
|
||||
use tui::layout::Alignment;
|
||||
use tui::widgets::{Block, Borders, Paragraph, Text, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
const SAMPLE_STRING: &str =
|
||||
"The library is based on the principle of immediate rendering with \
|
||||
intermediate buffers. This means that at each new frame you should build all widgets that are \
|
||||
supposed to be part of the UI. While providing a great flexibility for rich and \
|
||||
interactive UI, this may introduce overhead for highly dynamic content.";
|
||||
|
||||
#[test]
|
||||
fn paragraph_render_wrap() {
|
||||
let render = |alignment| {
|
||||
let backend = TestBackend::new(20, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
terminal
|
||||
.draw(|mut f| {
|
||||
let size = f.size();
|
||||
let text = [Text::raw(SAMPLE_STRING)];
|
||||
Paragraph::new(text.iter())
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.alignment(alignment)
|
||||
.wrap(true)
|
||||
.render(&mut f, size);
|
||||
})
|
||||
.unwrap();
|
||||
terminal.backend().buffer().clone()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
render(Alignment::Left),
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│The library is │",
|
||||
"│based on the │",
|
||||
"│principle of │",
|
||||
"│immediate │",
|
||||
"│rendering with │",
|
||||
"│intermediate │",
|
||||
"│buffers. This │",
|
||||
"│means that at each│",
|
||||
"└──────────────────┘",
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
render(Alignment::Right),
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│ The library is│",
|
||||
"│ based on the│",
|
||||
"│ principle of│",
|
||||
"│ immediate│",
|
||||
"│ rendering with│",
|
||||
"│ intermediate│",
|
||||
"│ buffers. This│",
|
||||
"│means that at each│",
|
||||
"└──────────────────┘",
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
render(Alignment::Center),
|
||||
Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│ The library is │",
|
||||
"│ based on the │",
|
||||
"│ principle of │",
|
||||
"│ immediate │",
|
||||
"│ rendering with │",
|
||||
"│ intermediate │",
|
||||
"│ buffers. This │",
|
||||
"│means that at each│",
|
||||
"└──────────────────┘",
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paragraph_render_double_width() {
|
||||
let backend = TestBackend::new(10, 10);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
let s = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点では、";
|
||||
terminal
|
||||
.draw(|mut f| {
|
||||
let size = f.size();
|
||||
let text = [Text::raw(s)];
|
||||
Paragraph::new(text.iter())
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.wrap(true)
|
||||
.render(&mut f, size);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected = Buffer::with_lines(vec![
|
||||
"┌────────┐",
|
||||
"│コンピュ│",
|
||||
"│ータ上で│",
|
||||
"│文字を扱│",
|
||||
"│う場合、│",
|
||||
"│典型的に│",
|
||||
"│は文字に│",
|
||||
"│よる通信│",
|
||||
"│を行う場│",
|
||||
"└────────┘",
|
||||
]);
|
||||
assert_eq!(&expected, terminal.backend().buffer());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paragraph_render_mixed_width() {
|
||||
let backend = TestBackend::new(10, 7);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
let s = "aコンピュータ上で文字を扱う場合、";
|
||||
terminal
|
||||
.draw(|mut f| {
|
||||
let size = f.size();
|
||||
let text = [Text::raw(s)];
|
||||
Paragraph::new(text.iter())
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.wrap(true)
|
||||
.render(&mut f, size);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected = Buffer::with_lines(vec![
|
||||
// The internal width is 8 so only 4 slots for double-width characters.
|
||||
"┌────────┐",
|
||||
"│aコンピ │", // Here we have 1 latin character so only 3 double-width ones can fit.
|
||||
"│ュータ上│",
|
||||
"│で文字を│",
|
||||
"│扱う場合│",
|
||||
"│、 │",
|
||||
"└────────┘",
|
||||
]);
|
||||
assert_eq!(&expected, terminal.backend().buffer());
|
||||
}
|
||||
11
tests/size.rs
Normal file
11
tests/size.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use tui::backend::{Backend, TestBackend};
|
||||
use tui::Terminal;
|
||||
|
||||
#[test]
|
||||
fn buffer_size_limited() {
|
||||
let backend = TestBackend::new(400, 400);
|
||||
let terminal = Terminal::new(backend).unwrap();
|
||||
let size = terminal.backend().size().unwrap();
|
||||
assert_eq!(size.width, 255);
|
||||
assert_eq!(size.height, 255);
|
||||
}
|
||||
Reference in New Issue
Block a user