Compare commits
17 Commits
540/patch-
...
feat-inlin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02213a25a8 | ||
|
|
853d9047b0 | ||
|
|
6069d89dee | ||
|
|
d25e263b8e | ||
|
|
d05e696d45 | ||
|
|
ef583cead9 | ||
|
|
90c4da4e68 | ||
|
|
8032191366 | ||
|
|
c8c03294e1 | ||
|
|
e00df22588 | ||
|
|
9806217a6a | ||
|
|
1be5cf2d90 | ||
|
|
ca68bae4ed | ||
|
|
8c1f58079f | ||
|
|
4845c03eec | ||
|
|
532a595c41 | ||
|
|
25ce5bc90b |
26
.github/workflows/ci.yml
vendored
26
.github/workflows/ci.yml
vendored
@@ -9,7 +9,7 @@ on:
|
||||
name: CI
|
||||
|
||||
env:
|
||||
CI_CARGO_MAKE_VERSION: 0.32.9
|
||||
CI_CARGO_MAKE_VERSION: 0.35.6
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
rust: ["1.44.0", "stable"]
|
||||
rust: ["1.52.1", "stable"]
|
||||
steps:
|
||||
- name: "Install dependencies"
|
||||
run: sudo apt-get install libncurses5-dev
|
||||
@@ -38,22 +38,16 @@ jobs:
|
||||
- name: "Install cargo-make"
|
||||
if: steps.cache-cargo-make.outputs.cache-hit != 'true'
|
||||
run: cargo install cargo-make --version ${{ env.CI_CARGO_MAKE_VERSION }}
|
||||
- name: "Format"
|
||||
run: cargo make fmt
|
||||
- name: "Check"
|
||||
run: cargo make check
|
||||
- name: "Test"
|
||||
run: cargo make test
|
||||
- name: "Format / Build / Test"
|
||||
run: cargo make ci
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
- name: "Clippy"
|
||||
run: cargo make clippy
|
||||
windows:
|
||||
name: Windows
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
rust: ["1.44.0", "stable"]
|
||||
rust: ["1.52.1", "stable"]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
|
||||
@@ -73,13 +67,7 @@ jobs:
|
||||
- name: "Install cargo-make"
|
||||
if: steps.cache-cargo-make.outputs.cache-hit != 'true'
|
||||
run: cargo install cargo-make --version ${{ env.CI_CARGO_MAKE_VERSION }}
|
||||
- name: "Format"
|
||||
run: cargo make fmt
|
||||
- name: "Check"
|
||||
run: cargo make check
|
||||
- name: "Test"
|
||||
run: cargo make test
|
||||
- name: "Format / Build / Test"
|
||||
run: cargo make ci
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
- name: "Clippy"
|
||||
run: cargo make clippy
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## To be released
|
||||
|
||||
### Features
|
||||
|
||||
* Add option to `widgets::List` to repeat the hightlight symbol for each line of multi-line items (#533).
|
||||
|
||||
## v0.16.0 - 2021-08-01
|
||||
|
||||
### Features
|
||||
|
||||
152
Cargo.toml
152
Cargo.toml
@@ -17,111 +17,79 @@ edition = "2018"
|
||||
[badges]
|
||||
|
||||
[features]
|
||||
default = ["termion"]
|
||||
curses = ["easycurses", "pancurses"]
|
||||
default = ["crossterm"]
|
||||
|
||||
[dependencies]
|
||||
bitflags = "1.0"
|
||||
tracing = "0.1"
|
||||
bitflags = "1.3"
|
||||
cassowary = "0.3"
|
||||
unicode-segmentation = "1.2"
|
||||
unicode-width = "0.1"
|
||||
termion = { version = "1.5", optional = true }
|
||||
rustbox = { version = "0.11", optional = true }
|
||||
crossterm = { version = "0.20", optional = true }
|
||||
easycurses = { version = "0.12.2", optional = true }
|
||||
pancurses = { version = "0.16.1", optional = true, features = ["win32a"] }
|
||||
serde = { version = "1", "optional" = true, features = ["derive"]}
|
||||
crossterm = { version = "0.22", optional = true }
|
||||
serde = { version = "1", optional = true, features = ["derive"]}
|
||||
|
||||
[dev-dependencies]
|
||||
tracing-subscriber = "0.2"
|
||||
rand = "0.8"
|
||||
argh = "0.1"
|
||||
|
||||
[[example]]
|
||||
name = "canvas"
|
||||
path = "examples/canvas.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "user_input"
|
||||
path = "examples/user_input.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "gauge"
|
||||
path = "examples/gauge.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "barchart"
|
||||
path = "examples/barchart.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "chart"
|
||||
path = "examples/chart.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "paragraph"
|
||||
path = "examples/paragraph.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "list"
|
||||
path = "examples/list.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "table"
|
||||
path = "examples/table.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "tabs"
|
||||
path = "examples/tabs.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "custom_widget"
|
||||
path = "examples/custom_widget.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "layout"
|
||||
path = "examples/layout.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "popup"
|
||||
path = "examples/popup.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "block"
|
||||
path = "examples/block.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
name = "sparkline"
|
||||
path = "examples/sparkline.rs"
|
||||
required-features = ["termion"]
|
||||
|
||||
[[example]]
|
||||
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_demo"
|
||||
path = "examples/crossterm_demo.rs"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "curses_demo"
|
||||
path = "examples/curses_demo.rs"
|
||||
required-features = ["curses"]
|
||||
name = "block"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "canvas"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "chart"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "custom_widget"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "gauge"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "layout"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "list"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "paragraph"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "popup"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "sparkline"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "table"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "tabs"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "user_input"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "inline"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
124
Makefile.toml
124
Makefile.toml
@@ -1,28 +1,34 @@
|
||||
[config]
|
||||
skip_core_tasks = true
|
||||
|
||||
[env.TUI_FEATURES]
|
||||
source = "${CARGO_MAKE_RUST_TARGET_OS}"
|
||||
default_value = "unknown"
|
||||
|
||||
[env.TUI_FEATURES.mapping]
|
||||
linux = "serde,crossterm,termion,rustbox,curses"
|
||||
macos = "serde,crossterm,termion,rustbox,curses"
|
||||
windows = "serde,crossterm"
|
||||
|
||||
[tasks.default]
|
||||
dependencies = [
|
||||
"check",
|
||||
]
|
||||
|
||||
[tasks.ci]
|
||||
dependencies = [
|
||||
"fmt",
|
||||
"check",
|
||||
"test",
|
||||
"clippy",
|
||||
run_task = [
|
||||
{ name = "ci-unix", condition = { platforms = ["linux", "mac"] } },
|
||||
{ name = "ci-windows", condition = { platforms = ["windows"] } },
|
||||
]
|
||||
|
||||
[tasks.ci-unix]
|
||||
private = true
|
||||
dependencies = [
|
||||
"fmt",
|
||||
"check-crossterm",
|
||||
"check-termion",
|
||||
"test-crossterm",
|
||||
"test-termion",
|
||||
"clippy-crossterm",
|
||||
"clippy-termion",
|
||||
"test-doc",
|
||||
]
|
||||
|
||||
[tasks.ci-windows]
|
||||
private = true
|
||||
dependencies = [
|
||||
"fmt",
|
||||
"check-crossterm",
|
||||
"test-crossterm",
|
||||
"clippy-crossterm",
|
||||
"test-doc",
|
||||
]
|
||||
|
||||
[tasks.fmt]
|
||||
command = "cargo"
|
||||
@@ -33,8 +39,17 @@ args = [
|
||||
"--check",
|
||||
]
|
||||
|
||||
[tasks.check-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm" }
|
||||
run_task = "check"
|
||||
|
||||
[tasks.check-termion]
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "check"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
args = [
|
||||
"check",
|
||||
"--no-default-features",
|
||||
@@ -43,8 +58,17 @@ args = [
|
||||
"--all-targets",
|
||||
]
|
||||
|
||||
[tasks.build-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm" }
|
||||
run_task = "build"
|
||||
|
||||
[tasks.build-termion]
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "build"
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
args = [
|
||||
"build",
|
||||
"--no-default-features",
|
||||
@@ -53,10 +77,20 @@ args = [
|
||||
"--all-targets",
|
||||
]
|
||||
|
||||
[tasks.clippy-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm" }
|
||||
run_task = "clippy"
|
||||
|
||||
[tasks.clippy-termion]
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "clippy"
|
||||
|
||||
[tasks.clippy]
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
args = [
|
||||
"clippy",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
@@ -65,23 +99,17 @@ args = [
|
||||
"warnings",
|
||||
]
|
||||
|
||||
[tasks.test-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm" }
|
||||
run_task = "test"
|
||||
|
||||
[tasks.test-termion]
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "test"
|
||||
|
||||
[tasks.test]
|
||||
linux_alias = "test-unix"
|
||||
mac_alias = "test-unix"
|
||||
windows_alias = "test-windows"
|
||||
|
||||
[tasks.test-unix]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${TUI_FEATURES}"
|
||||
]
|
||||
|
||||
# Documentation tests cannot be run on Windows for now
|
||||
[tasks.test-windows]
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
args = [
|
||||
"test",
|
||||
"--no-default-features",
|
||||
@@ -92,39 +120,29 @@ args = [
|
||||
"--examples",
|
||||
]
|
||||
|
||||
[tasks.test-doc]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test",
|
||||
"--doc",
|
||||
]
|
||||
|
||||
[tasks.run-example]
|
||||
private = true
|
||||
condition = { env_set = ["TUI_EXAMPLE_NAME", "TUI_FEATURES"] }
|
||||
condition = { env_set = ["TUI_EXAMPLE_NAME"] }
|
||||
command = "cargo"
|
||||
args = [
|
||||
"run",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--release",
|
||||
"--example",
|
||||
"${TUI_EXAMPLE_NAME}"
|
||||
]
|
||||
|
||||
[tasks.run-example-windows]
|
||||
private = true
|
||||
condition = { env = {"TUI_EXAMPLE_NAME" = "crossterm_demo"} }
|
||||
run_task = "run-example"
|
||||
|
||||
[tasks.run-example-router]
|
||||
private = true
|
||||
run_task = [
|
||||
{ name = "run-example-windows", condition = { platforms = ["window"] } },
|
||||
{ name = "run-example" }
|
||||
]
|
||||
|
||||
[tasks.build-examples]
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
command = "cargo"
|
||||
args = [
|
||||
"build",
|
||||
"--examples",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--release"
|
||||
]
|
||||
|
||||
@@ -137,6 +155,6 @@ for file in ${files}
|
||||
name = basename ${file}
|
||||
name = substring ${name} -3
|
||||
set_env TUI_EXAMPLE_NAME ${name}
|
||||
cm_run_task run-example-router
|
||||
cm_run_task run-example
|
||||
end
|
||||
'''
|
||||
|
||||
38
README.md
38
README.md
@@ -11,15 +11,9 @@ 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 four different backends to draw to the terminal. You
|
||||
can either choose from:
|
||||
|
||||
The library supports multiple backends:
|
||||
- [crossterm](https://github.com/crossterm-rs/crossterm) [default]
|
||||
- [termion](https://github.com/ticki/termion)
|
||||
- [rustbox](https://github.com/gchp/rustbox)
|
||||
- [crossterm](https://github.com/crossterm-rs/crossterm)
|
||||
- [pancurses](https://github.com/ihalila/pancurses)
|
||||
|
||||
However, some features may only be available in one of the four.
|
||||
|
||||
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,18 +28,19 @@ you may rely on the previously cited libraries to achieve such features.
|
||||
|
||||
### Rust version requirements
|
||||
|
||||
Since version 0.10.0, `tui` requires **rustc version 1.44.0 or greater**.
|
||||
Since version 0.17.0, `tui` requires **rustc version 1.52.1 or greater**.
|
||||
|
||||
### [Documentation](https://docs.rs/tui)
|
||||
|
||||
### Demo
|
||||
|
||||
The demo shown in the gif can be run with all available backends
|
||||
(`examples/*_demo.rs` files). For example to see the `termion` version one could
|
||||
run:
|
||||
The demo shown in the gif can be run with all available backends.
|
||||
|
||||
```
|
||||
cargo run --example termion_demo --release -- --tick-rate 200
|
||||
# crossterm
|
||||
cargo run --example demo --release -- --tick-rate 200
|
||||
# termion
|
||||
cargo run --example demo --no-default-features --features=termion --release -- --tick-rate 200
|
||||
```
|
||||
|
||||
where `tick-rate` is the UI refresh rate in ms.
|
||||
@@ -53,18 +48,11 @@ where `tick-rate` is the UI refresh rate in ms.
|
||||
The UI code is in [examples/demo/ui.rs](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/demo/ui.rs) while the
|
||||
application state is in [examples/demo/app.rs](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/demo/app.rs).
|
||||
|
||||
Beware that the `termion_demo` only works on Unix platforms. If you are a Windows user,
|
||||
you can see the same demo using the `crossterm` backend with the following command:
|
||||
|
||||
```
|
||||
cargo run --example crossterm_demo --no-default-features --features="crossterm" --release -- --tick-rate 200
|
||||
```
|
||||
|
||||
If the user interface contains glyphs that are not displayed correctly by your terminal, you may want to run
|
||||
the demo without those symbols:
|
||||
|
||||
```
|
||||
cargo run --example crossterm_demo --no-default-features --features="crossterm" --release -- --tick-rate 200 --enhanced-graphics false
|
||||
cargo run --example demo --release -- --tick-rate 200 --enhanced-graphics false
|
||||
```
|
||||
|
||||
### Widgets
|
||||
@@ -83,9 +71,10 @@ The library comes with the following list of widgets:
|
||||
* [Tabs](https://github.com/fdehau/tui-rs/blob/v0.16.0/examples/tabs.rs)
|
||||
|
||||
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`.
|
||||
cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by pressing `q`.
|
||||
|
||||
You can run all examples by running `make run-examples`.
|
||||
You can run all examples by running `cargo make run-examples` (require
|
||||
`cargo-make` that can be installed with `cargo install cargo-make`).
|
||||
|
||||
### Third-party widgets
|
||||
|
||||
@@ -116,6 +105,9 @@ You can run all examples by running `make run-examples`.
|
||||
* [minesweep](https://github.com/cpcloud/minesweep-rs)
|
||||
* [Battleship.rs](https://github.com/deepu105/battleship-rs)
|
||||
* [termscp](https://github.com/veeso/termscp)
|
||||
* [joshuto](https://github.com/kamiyaa/joshuto)
|
||||
* [adsb_deku/radar](https://github.com/wcampbell0x2a/adsb_deku#radar-tui)
|
||||
* [hoard](https://github.com/Hyde46/hoard)
|
||||
|
||||
### Alternatives
|
||||
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{BarChart, Block, Borders},
|
||||
Terminal,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App<'a> {
|
||||
@@ -48,85 +52,110 @@ impl<'a> App<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
fn on_tick(&mut self) {
|
||||
let value = self.data.pop().unwrap();
|
||||
self.data.insert(0, value);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn 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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// Setup event handlers
|
||||
let events = Events::new();
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
loop {
|
||||
terminal.draw(|f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().title("Data1").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.bar_width(9)
|
||||
.bar_style(Style::default().fg(Color::Yellow))
|
||||
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
|
||||
f.render_widget(barchart, chunks[0]);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[1]);
|
||||
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().title("Data2").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.bar_width(5)
|
||||
.bar_gap(3)
|
||||
.bar_style(Style::default().fg(Color::Green))
|
||||
.value_style(
|
||||
Style::default()
|
||||
.bg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
f.render_widget(barchart, chunks[0]);
|
||||
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().title("Data3").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.bar_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)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
);
|
||||
f.render_widget(barchart, chunks[1]);
|
||||
})?;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(input) => {
|
||||
if input == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Tick => {
|
||||
app.update();
|
||||
}
|
||||
}
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().title("Data1").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.bar_width(9)
|
||||
.bar_style(Style::default().fg(Color::Yellow))
|
||||
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
|
||||
f.render_widget(barchart, chunks[0]);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[1]);
|
||||
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().title("Data2").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.bar_width(5)
|
||||
.bar_gap(3)
|
||||
.bar_style(Style::default().fg(Color::Green))
|
||||
.value_style(
|
||||
Style::default()
|
||||
.bg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
f.render_widget(barchart, chunks[0]);
|
||||
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().title("Data3").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.bar_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)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
);
|
||||
f.render_widget(barchart, chunks[1]);
|
||||
}
|
||||
|
||||
@@ -1,101 +1,119 @@
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
widgets::{Block, BorderType, Borders},
|
||||
Terminal,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
fn main() -> Result<(), Box<dyn 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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// Setup event handlers
|
||||
let events = Events::new();
|
||||
// create app and run it
|
||||
let res = run_app(&mut terminal);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|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();
|
||||
terminal.draw(ui)?;
|
||||
|
||||
// Surrounding block
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Main block with round corners")
|
||||
.title_alignment(Alignment::Center)
|
||||
.border_type(BorderType::Rounded);
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(4)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
// Top two inner blocks
|
||||
let top_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[0]);
|
||||
|
||||
// Top left inner block with green background
|
||||
let block = Block::default()
|
||||
.title(vec![
|
||||
Span::styled("With", Style::default().fg(Color::Yellow)),
|
||||
Span::from(" background"),
|
||||
])
|
||||
.style(Style::default().bg(Color::Green));
|
||||
f.render_widget(block, top_chunks[0]);
|
||||
|
||||
// Top right inner block with styled title aligned to the right
|
||||
let block = Block::default()
|
||||
.title(Span::styled(
|
||||
"Styled title",
|
||||
Style::default()
|
||||
.fg(Color::White)
|
||||
.bg(Color::Red)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.title_alignment(Alignment::Right);
|
||||
f.render_widget(block, top_chunks[1]);
|
||||
|
||||
// Bottom two inner blocks
|
||||
let bottom_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[1]);
|
||||
|
||||
// Bottom left block with all default borders
|
||||
let block = Block::default().title("With borders").borders(Borders::ALL);
|
||||
f.render_widget(block, bottom_chunks[0]);
|
||||
|
||||
// Bottom right block with styled left and right border
|
||||
let block = Block::default()
|
||||
.title("With styled borders and doubled borders")
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.border_type(BorderType::Double);
|
||||
f.render_widget(block, bottom_chunks[1]);
|
||||
})?;
|
||||
|
||||
if let Event::Input(key) = events.next()? {
|
||||
if key == Key::Char('q') {
|
||||
break;
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>) {
|
||||
// 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();
|
||||
|
||||
// Surrounding block
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Main block with round corners")
|
||||
.title_alignment(Alignment::Center)
|
||||
.border_type(BorderType::Rounded);
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(4)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
// Top two inner blocks
|
||||
let top_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[0]);
|
||||
|
||||
// Top left inner block with green background
|
||||
let block = Block::default()
|
||||
.title(vec![
|
||||
Span::styled("With", Style::default().fg(Color::Yellow)),
|
||||
Span::from(" background"),
|
||||
])
|
||||
.style(Style::default().bg(Color::Green));
|
||||
f.render_widget(block, top_chunks[0]);
|
||||
|
||||
// Top right inner block with styled title aligned to the right
|
||||
let block = Block::default()
|
||||
.title(Span::styled(
|
||||
"Styled title",
|
||||
Style::default()
|
||||
.fg(Color::White)
|
||||
.bg(Color::Red)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.title_alignment(Alignment::Right);
|
||||
f.render_widget(block, top_chunks[1]);
|
||||
|
||||
// Bottom two inner blocks
|
||||
let bottom_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[1]);
|
||||
|
||||
// Bottom left block with all default borders
|
||||
let block = Block::default().title("With borders").borders(Borders::ALL);
|
||||
f.render_widget(block, bottom_chunks[0]);
|
||||
|
||||
// Bottom right block with styled left and right border
|
||||
let block = Block::default()
|
||||
.title("With styled borders and doubled borders")
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.border_type(BorderType::Double);
|
||||
f.render_widget(block, bottom_chunks[1]);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::event::{Config, Event, Events};
|
||||
use std::{error::Error, io, time::Duration};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::Color,
|
||||
style::{Color, Style},
|
||||
text::Span,
|
||||
widgets::{
|
||||
canvas::{Canvas, Map, MapResolution, Rectangle},
|
||||
Block, Borders,
|
||||
},
|
||||
Terminal,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App {
|
||||
@@ -46,7 +51,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
fn on_tick(&mut self) {
|
||||
if self.ball.x < self.playground.left() as f64
|
||||
|| self.ball.x + self.ball.width > self.playground.right() as f64
|
||||
{
|
||||
@@ -73,76 +78,103 @@ impl App {
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn 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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// Setup event handlers
|
||||
let config = Config {
|
||||
tick_rate: Duration::from_millis(250),
|
||||
..Default::default()
|
||||
};
|
||||
let events = Events::with_config(config);
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
loop {
|
||||
terminal.draw(|f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
let canvas = 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]);
|
||||
f.render_widget(canvas, chunks[0]);
|
||||
let canvas = Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Pong"))
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&app.ball);
|
||||
})
|
||||
.x_bounds([10.0, 110.0])
|
||||
.y_bounds([10.0, 110.0]);
|
||||
f.render_widget(canvas, chunks[1]);
|
||||
})?;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(input) => match input {
|
||||
Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
Key::Down => {
|
||||
app.y += 1.0;
|
||||
}
|
||||
Key::Up => {
|
||||
app.y -= 1.0;
|
||||
}
|
||||
Key::Right => {
|
||||
app.x += 1.0;
|
||||
}
|
||||
Key::Left => {
|
||||
app.x -= 1.0;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
},
|
||||
Event::Tick => {
|
||||
app.update();
|
||||
}
|
||||
}
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => {
|
||||
return Ok(());
|
||||
}
|
||||
KeyCode::Down => {
|
||||
app.y += 1.0;
|
||||
}
|
||||
KeyCode::Up => {
|
||||
app.y -= 1.0;
|
||||
}
|
||||
KeyCode::Right => {
|
||||
app.x += 1.0;
|
||||
}
|
||||
KeyCode::Left => {
|
||||
app.x -= 1.0;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
let canvas = 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,
|
||||
Span::styled("You are here", Style::default().fg(Color::Yellow)),
|
||||
);
|
||||
})
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0]);
|
||||
f.render_widget(canvas, chunks[0]);
|
||||
let canvas = Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Pong"))
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&app.ball);
|
||||
})
|
||||
.x_bounds([10.0, 110.0])
|
||||
.y_bounds([10.0, 110.0]);
|
||||
f.render_widget(canvas, chunks[1]);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::{
|
||||
event::{Event, Events},
|
||||
SinSignal,
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
text::Span,
|
||||
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType},
|
||||
Terminal,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
const DATA: [(f64, f64); 5] = [(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0)];
|
||||
@@ -28,6 +29,34 @@ const DATA2: [(f64, f64); 7] = [
|
||||
(60.0, 3.0),
|
||||
];
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SinSignal {
|
||||
x: f64,
|
||||
interval: f64,
|
||||
period: f64,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
impl SinSignal {
|
||||
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
|
||||
SinSignal {
|
||||
x: 0.0,
|
||||
interval,
|
||||
period,
|
||||
scale,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for SinSignal {
|
||||
type Item = (f64, f64);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
|
||||
self.x += self.interval;
|
||||
Some(point)
|
||||
}
|
||||
}
|
||||
|
||||
struct App {
|
||||
signal1: SinSignal,
|
||||
data1: Vec<(f64, f64)>,
|
||||
@@ -51,7 +80,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
fn on_tick(&mut self) {
|
||||
for _ in 0..5 {
|
||||
self.data1.remove(0);
|
||||
}
|
||||
@@ -66,181 +95,207 @@ impl App {
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn 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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let events = Events::new();
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
loop {
|
||||
terminal.draw(|f| {
|
||||
let size = f.size();
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(size);
|
||||
let x_labels = vec![
|
||||
Span::styled(
|
||||
format!("{}", app.window[0]),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(format!("{}", (app.window[0] + app.window[1]) / 2.0)),
|
||||
Span::styled(
|
||||
format!("{}", app.window[1]),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
];
|
||||
let datasets = vec![
|
||||
Dataset::default()
|
||||
.name("data2")
|
||||
.marker(symbols::Marker::Dot)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.data(&app.data1),
|
||||
Dataset::default()
|
||||
.name("data3")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.data(&app.data2),
|
||||
];
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(Span::styled(
|
||||
"Chart 1",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels(x_labels)
|
||||
.bounds(app.window),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels(vec![
|
||||
Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw("0"),
|
||||
Span::styled("20", Style::default().add_modifier(Modifier::BOLD)),
|
||||
])
|
||||
.bounds([-20.0, 20.0]),
|
||||
);
|
||||
f.render_widget(chart, chunks[0]);
|
||||
|
||||
let datasets = vec![Dataset::default()
|
||||
.name("data")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&DATA)];
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(Span::styled(
|
||||
"Chart 2",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec![
|
||||
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw("2.5"),
|
||||
Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec![
|
||||
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw("2.5"),
|
||||
Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
);
|
||||
f.render_widget(chart, chunks[1]);
|
||||
|
||||
let datasets = vec![Dataset::default()
|
||||
.name("data")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&DATA2)];
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(Span::styled(
|
||||
"Chart 3",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 50.0])
|
||||
.labels(vec![
|
||||
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw("25"),
|
||||
Span::styled("50", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec![
|
||||
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw("2.5"),
|
||||
Span::styled("5", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
);
|
||||
f.render_widget(chart, chunks[2]);
|
||||
})?;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(input) => {
|
||||
if input == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Tick => {
|
||||
app.update();
|
||||
}
|
||||
}
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let size = f.size();
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(size);
|
||||
let x_labels = vec![
|
||||
Span::styled(
|
||||
format!("{}", app.window[0]),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(format!("{}", (app.window[0] + app.window[1]) / 2.0)),
|
||||
Span::styled(
|
||||
format!("{}", app.window[1]),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
];
|
||||
let datasets = vec![
|
||||
Dataset::default()
|
||||
.name("data2")
|
||||
.marker(symbols::Marker::Dot)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.data(&app.data1),
|
||||
Dataset::default()
|
||||
.name("data3")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.data(&app.data2),
|
||||
];
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(Span::styled(
|
||||
"Chart 1",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels(x_labels)
|
||||
.bounds(app.window),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels(vec![
|
||||
Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw("0"),
|
||||
Span::styled("20", Style::default().add_modifier(Modifier::BOLD)),
|
||||
])
|
||||
.bounds([-20.0, 20.0]),
|
||||
);
|
||||
f.render_widget(chart, chunks[0]);
|
||||
|
||||
let datasets = vec![Dataset::default()
|
||||
.name("data")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&DATA)];
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(Span::styled(
|
||||
"Chart 2",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec![
|
||||
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw("2.5"),
|
||||
Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec![
|
||||
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw("2.5"),
|
||||
Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
);
|
||||
f.render_widget(chart, chunks[1]);
|
||||
|
||||
let datasets = vec![Dataset::default()
|
||||
.name("data")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&DATA2)];
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(Span::styled(
|
||||
"Chart 3",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 50.0])
|
||||
.labels(vec![
|
||||
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw("25"),
|
||||
Span::styled("50", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec![
|
||||
Span::styled("0", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw("2.5"),
|
||||
Span::styled("5", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
);
|
||||
f.render_widget(chart, chunks[2]);
|
||||
}
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
#[allow(dead_code)]
|
||||
mod demo;
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::demo::{ui, App};
|
||||
use argh::FromArgs;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event as CEvent, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io::stdout,
|
||||
sync::mpsc,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{backend::CrosstermBackend, Terminal};
|
||||
|
||||
enum Event<I> {
|
||||
Input(I),
|
||||
Tick,
|
||||
}
|
||||
|
||||
/// Crossterm demo
|
||||
#[derive(Debug, FromArgs)]
|
||||
struct Cli {
|
||||
/// time in ms between two ticks.
|
||||
#[argh(option, default = "250")]
|
||||
tick_rate: u64,
|
||||
/// whether unicode symbols are used to improve the overall look of the app
|
||||
#[argh(option, default = "true")]
|
||||
enhanced_graphics: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let cli: Cli = argh::from_env();
|
||||
|
||||
enable_raw_mode()?;
|
||||
|
||||
let mut stdout = stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// Setup input handling
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
let tick_rate = Duration::from_millis(cli.tick_rate);
|
||||
thread::spawn(move || {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
// poll for tick rate duration, if no events, sent tick event.
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if event::poll(timeout).unwrap() {
|
||||
if let CEvent::Key(key) = event::read().unwrap() {
|
||||
tx.send(Event::Input(key)).unwrap();
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
tx.send(Event::Tick).unwrap();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut app = App::new("Crossterm Demo", cli.enhanced_graphics);
|
||||
|
||||
terminal.clear()?;
|
||||
|
||||
loop {
|
||||
terminal.draw(|f| ui::draw(f, &mut app))?;
|
||||
match rx.recv()? {
|
||||
Event::Input(event) => match event.code {
|
||||
KeyCode::Char('q') => {
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
break;
|
||||
}
|
||||
KeyCode::Char(c) => app.on_key(c),
|
||||
KeyCode::Left => app.on_left(),
|
||||
KeyCode::Up => app.on_up(),
|
||||
KeyCode::Right => app.on_right(),
|
||||
KeyCode::Down => app.on_down(),
|
||||
_ => {}
|
||||
},
|
||||
Event::Tick => {
|
||||
app.on_tick();
|
||||
}
|
||||
}
|
||||
if app.should_quit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
mod demo;
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::demo::{ui, App};
|
||||
use argh::FromArgs;
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{backend::CursesBackend, Terminal};
|
||||
|
||||
/// Curses demo
|
||||
#[derive(Debug, FromArgs)]
|
||||
struct Cli {
|
||||
/// time in ms between two ticks.
|
||||
#[argh(option, default = "250")]
|
||||
tick_rate: u64,
|
||||
/// whether unicode symbols are used to improve the overall look of the app
|
||||
#[argh(option, default = "true")]
|
||||
enhanced_graphics: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let cli: Cli = argh::from_env();
|
||||
|
||||
let mut backend =
|
||||
CursesBackend::new().ok_or_else(|| 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", cli.enhanced_graphics);
|
||||
|
||||
let mut last_tick = Instant::now();
|
||||
let tick_rate = Duration::from_millis(cli.tick_rate);
|
||||
loop {
|
||||
terminal.draw(|f| ui::draw(f, &mut app))?;
|
||||
if let Some(input) = terminal.backend_mut().get_curses_mut().get_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,23 +1,23 @@
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::TermionBackend, buffer::Buffer, layout::Rect, style::Style, widgets::Widget, Terminal,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
widgets::Widget,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Label<'a> {
|
||||
text: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Default for Label<'a> {
|
||||
fn default() -> Label<'a> {
|
||||
Label { text: "" }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Label<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_string(area.left(), area.top(), self.text, Style::default());
|
||||
@@ -32,28 +32,46 @@ impl<'a> Label<'a> {
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn 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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let events = Events::new();
|
||||
// create app and run it
|
||||
let res = run_app(&mut terminal);
|
||||
|
||||
loop {
|
||||
terminal.draw(|f| {
|
||||
let size = f.size();
|
||||
let label = Label::default().text("Test");
|
||||
f.render_widget(label, size);
|
||||
})?;
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Event::Input(key) = events.next()? {
|
||||
if key == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(ui)?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>) {
|
||||
let size = f.size();
|
||||
let label = Label::default().text("Test");
|
||||
f.render_widget(label, size);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use crate::util::{RandomSignal, SinSignal, StatefulList, TabsState};
|
||||
use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use tui::widgets::ListState;
|
||||
|
||||
const TASKS: [&str; 24] = [
|
||||
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10",
|
||||
@@ -62,6 +66,120 @@ const EVENTS: [(&str, u64); 24] = [
|
||||
("B24", 5),
|
||||
];
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RandomSignal {
|
||||
distribution: Uniform<u64>,
|
||||
rng: ThreadRng,
|
||||
}
|
||||
|
||||
impl RandomSignal {
|
||||
pub fn new(lower: u64, upper: u64) -> RandomSignal {
|
||||
RandomSignal {
|
||||
distribution: Uniform::new(lower, upper),
|
||||
rng: rand::thread_rng(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for RandomSignal {
|
||||
type Item = u64;
|
||||
fn next(&mut self) -> Option<u64> {
|
||||
Some(self.distribution.sample(&mut self.rng))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SinSignal {
|
||||
x: f64,
|
||||
interval: f64,
|
||||
period: f64,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
impl SinSignal {
|
||||
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
|
||||
SinSignal {
|
||||
x: 0.0,
|
||||
interval,
|
||||
period,
|
||||
scale,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for SinSignal {
|
||||
type Item = (f64, f64);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
|
||||
self.x += self.interval;
|
||||
Some(point)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TabsState<'a> {
|
||||
pub titles: Vec<&'a str>,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
impl<'a> TabsState<'a> {
|
||||
pub fn new(titles: Vec<&'a str>) -> TabsState {
|
||||
TabsState { titles, index: 0 }
|
||||
}
|
||||
pub fn next(&mut self) {
|
||||
self.index = (self.index + 1) % self.titles.len();
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
if self.index > 0 {
|
||||
self.index -= 1;
|
||||
} else {
|
||||
self.index = self.titles.len() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StatefulList<T> {
|
||||
pub state: ListState,
|
||||
pub items: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T> StatefulList<T> {
|
||||
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
|
||||
StatefulList {
|
||||
state: ListState::default(),
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Signal<S: Iterator> {
|
||||
source: S,
|
||||
pub points: Vec<S::Item>,
|
||||
|
||||
77
examples/demo/crossterm.rs
Normal file
77
examples/demo/crossterm.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use crate::{app::App, ui};
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new("Crossterm Demo", enhanced_graphics);
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui::draw(f, &mut app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char(c) => app.on_key(c),
|
||||
KeyCode::Left => app.on_left(),
|
||||
KeyCode::Up => app.on_up(),
|
||||
KeyCode::Right => app.on_right(),
|
||||
KeyCode::Down => app.on_down(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
if app.should_quit {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
31
examples/demo/main.rs
Normal file
31
examples/demo/main.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
mod app;
|
||||
#[cfg(feature = "crossterm")]
|
||||
mod crossterm;
|
||||
#[cfg(feature = "termion")]
|
||||
mod termion;
|
||||
mod ui;
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
use crate::crossterm::run;
|
||||
#[cfg(feature = "termion")]
|
||||
use crate::termion::run;
|
||||
use argh::FromArgs;
|
||||
use std::{error::Error, time::Duration};
|
||||
|
||||
/// Demo
|
||||
#[derive(Debug, FromArgs)]
|
||||
struct Cli {
|
||||
/// time in ms between two ticks.
|
||||
#[argh(option, default = "250")]
|
||||
tick_rate: u64,
|
||||
/// whether unicode symbols are used to improve the overall look of the app
|
||||
#[argh(option, default = "true")]
|
||||
enhanced_graphics: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let cli: Cli = argh::from_env();
|
||||
let tick_rate = Duration::from_millis(cli.tick_rate);
|
||||
run(tick_rate, cli.enhanced_graphics)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
mod app;
|
||||
pub mod ui;
|
||||
pub use app::App;
|
||||
80
examples/demo/termion.rs
Normal file
80
examples/demo/termion.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use crate::{app::App, ui};
|
||||
use std::{error::Error, io, sync::mpsc, thread, time::Duration};
|
||||
use termion::{
|
||||
event::Key,
|
||||
input::{MouseTerminal, TermRead},
|
||||
raw::IntoRawMode,
|
||||
screen::AlternateScreen,
|
||||
};
|
||||
use tui::{
|
||||
backend::{Backend, TermionBackend},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
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)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new("Termion demo", enhanced_graphics);
|
||||
run_app(&mut terminal, app, tick_rate)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let events = events(tick_rate);
|
||||
loop {
|
||||
terminal.draw(|f| ui::draw(f, &mut app))?;
|
||||
|
||||
match events.recv()? {
|
||||
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 {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(Key),
|
||||
Tick,
|
||||
}
|
||||
|
||||
fn events(tick_rate: Duration) -> mpsc::Receiver<Event> {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let keys_tx = tx.clone();
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for key in stdin.keys().flatten() {
|
||||
if let Err(err) = keys_tx.send(Event::Input(key)) {
|
||||
eprintln!("{}", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
thread::spawn(move || loop {
|
||||
if let Err(err) = tx.send(Event::Tick) {
|
||||
eprintln!("{}", err);
|
||||
break;
|
||||
}
|
||||
thread::sleep(tick_rate);
|
||||
});
|
||||
rx
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::demo::App;
|
||||
use crate::app::App;
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
@@ -363,7 +363,11 @@ where
|
||||
} else {
|
||||
Color::Red
|
||||
};
|
||||
ctx.print(server.coords.1, server.coords.0, "X", color);
|
||||
ctx.print(
|
||||
server.coords.1,
|
||||
server.coords.0,
|
||||
Span::styled("X", Style::default().fg(color)),
|
||||
);
|
||||
}
|
||||
})
|
||||
.marker(if app.enhanced_graphics {
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::event::{Config, Event, Events};
|
||||
use std::{error::Error, io, time::Duration};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
widgets::{Block, Borders, Gauge},
|
||||
Terminal,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App {
|
||||
@@ -30,7 +34,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
fn on_tick(&mut self) {
|
||||
self.progress1 += 1;
|
||||
if self.progress1 > 100 {
|
||||
self.progress1 = 0;
|
||||
@@ -51,87 +55,112 @@ impl App {
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn 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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let events = Events::with_config(Config {
|
||||
tick_rate: Duration::from_millis(100),
|
||||
});
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
let mut app = App::new();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
loop {
|
||||
terminal.draw(|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 gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge1").borders(Borders::ALL))
|
||||
.gauge_style(Style::default().fg(Color::Yellow))
|
||||
.percent(app.progress1);
|
||||
f.render_widget(gauge, chunks[0]);
|
||||
|
||||
let label = format!("{}/100", app.progress2);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge2").borders(Borders::ALL))
|
||||
.gauge_style(Style::default().fg(Color::Magenta).bg(Color::Green))
|
||||
.percent(app.progress2)
|
||||
.label(label);
|
||||
f.render_widget(gauge, chunks[1]);
|
||||
|
||||
let label = Span::styled(
|
||||
format!("{:.2}%", app.progress3 * 100.0),
|
||||
Style::default()
|
||||
.fg(Color::Red)
|
||||
.add_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge3").borders(Borders::ALL))
|
||||
.gauge_style(Style::default().fg(Color::Yellow))
|
||||
.ratio(app.progress3)
|
||||
.label(label)
|
||||
.use_unicode(true);
|
||||
f.render_widget(gauge, chunks[2]);
|
||||
|
||||
let label = format!("{}/100", app.progress2);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge4"))
|
||||
.gauge_style(
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
)
|
||||
.percent(app.progress4)
|
||||
.label(label);
|
||||
f.render_widget(gauge, chunks[3]);
|
||||
})?;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(input) => {
|
||||
if input == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Tick => {
|
||||
app.update();
|
||||
}
|
||||
}
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
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 gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge1").borders(Borders::ALL))
|
||||
.gauge_style(Style::default().fg(Color::Yellow))
|
||||
.percent(app.progress1);
|
||||
f.render_widget(gauge, chunks[0]);
|
||||
|
||||
let label = format!("{}/100", app.progress2);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge2").borders(Borders::ALL))
|
||||
.gauge_style(Style::default().fg(Color::Magenta).bg(Color::Green))
|
||||
.percent(app.progress2)
|
||||
.label(label);
|
||||
f.render_widget(gauge, chunks[1]);
|
||||
|
||||
let label = Span::styled(
|
||||
format!("{:.2}%", app.progress3 * 100.0),
|
||||
Style::default()
|
||||
.fg(Color::Red)
|
||||
.add_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge3").borders(Borders::ALL))
|
||||
.gauge_style(Style::default().fg(Color::Yellow))
|
||||
.ratio(app.progress3)
|
||||
.label(label)
|
||||
.use_unicode(true);
|
||||
f.render_widget(gauge, chunks[2]);
|
||||
|
||||
let label = format!("{}/100", app.progress2);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge4"))
|
||||
.gauge_style(
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
)
|
||||
.percent(app.progress4)
|
||||
.label(label);
|
||||
f.render_widget(gauge, chunks[3]);
|
||||
}
|
||||
|
||||
311
examples/inline.rs
Normal file
311
examples/inline.rs
Normal file
@@ -0,0 +1,311 @@
|
||||
use rand::distributions::{Distribution, Uniform};
|
||||
use std::{
|
||||
collections::{BTreeMap, VecDeque},
|
||||
error::Error,
|
||||
io,
|
||||
sync::mpsc,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tracing::{event, span, Level};
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Gauge, LineGauge, List, ListItem, Paragraph, Widget},
|
||||
Frame, Terminal, TerminalOptions, ViewportVariant,
|
||||
};
|
||||
|
||||
const NUM_DOWNLOADS: usize = 10;
|
||||
|
||||
type DownloadId = usize;
|
||||
type WorkerId = usize;
|
||||
|
||||
enum Event {
|
||||
Input(crossterm::event::KeyEvent),
|
||||
Tick,
|
||||
Resize,
|
||||
DownloadUpdate(WorkerId, DownloadId, f64),
|
||||
DownloadDone(WorkerId, DownloadId),
|
||||
}
|
||||
|
||||
struct Downloads {
|
||||
pending: VecDeque<Download>,
|
||||
in_progress: BTreeMap<WorkerId, DownloadInProgress>,
|
||||
}
|
||||
|
||||
impl Downloads {
|
||||
fn next(&mut self, worker_id: WorkerId) -> Option<Download> {
|
||||
match self.pending.pop_front() {
|
||||
Some(d) => {
|
||||
self.in_progress.insert(
|
||||
worker_id,
|
||||
DownloadInProgress {
|
||||
id: d.id,
|
||||
started_at: Instant::now(),
|
||||
progress: 0.0,
|
||||
},
|
||||
);
|
||||
Some(d)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DownloadInProgress {
|
||||
id: DownloadId,
|
||||
started_at: Instant,
|
||||
progress: f64,
|
||||
}
|
||||
|
||||
struct Download {
|
||||
id: DownloadId,
|
||||
size: usize,
|
||||
}
|
||||
|
||||
struct Worker {
|
||||
id: WorkerId,
|
||||
tx: mpsc::Sender<Download>,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||
.with_writer(io::stderr)
|
||||
.init();
|
||||
|
||||
crossterm::terminal::enable_raw_mode()?;
|
||||
let stdout = io::stdout();
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: ViewportVariant::Inline(8),
|
||||
},
|
||||
)?;
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
input_handling(tx.clone());
|
||||
let workers = workers(tx);
|
||||
let mut downloads = downloads();
|
||||
|
||||
for w in &workers {
|
||||
let d = downloads.next(w.id).unwrap();
|
||||
w.tx.send(d).unwrap();
|
||||
}
|
||||
|
||||
run_app(&mut terminal, workers, downloads, rx)?;
|
||||
|
||||
crossterm::terminal::disable_raw_mode()?;
|
||||
terminal.clear()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn input_handling(tx: mpsc::Sender<Event>) {
|
||||
let tick_rate = Duration::from_millis(200);
|
||||
thread::spawn(move || {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
// poll for tick rate duration, if no events, sent tick event.
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout).unwrap() {
|
||||
match crossterm::event::read().unwrap() {
|
||||
crossterm::event::Event::Key(key) => tx.send(Event::Input(key)).unwrap(),
|
||||
crossterm::event::Event::Resize(_, _) => tx.send(Event::Resize).unwrap(),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
tx.send(Event::Tick).unwrap();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn workers(tx: mpsc::Sender<Event>) -> Vec<Worker> {
|
||||
(0..4)
|
||||
.map(|id| {
|
||||
let (worker_tx, worker_rx) = mpsc::channel::<Download>();
|
||||
let tx = tx.clone();
|
||||
thread::spawn(move || {
|
||||
while let Ok(download) = worker_rx.recv() {
|
||||
let mut remaining = download.size;
|
||||
while remaining > 0 {
|
||||
let wait = (remaining as u64).min(10);
|
||||
thread::sleep(Duration::from_millis(wait * 10));
|
||||
remaining = remaining.saturating_sub(10);
|
||||
let progress = (download.size - remaining) * 100 / download.size;
|
||||
tx.send(Event::DownloadUpdate(id, download.id, progress as f64))
|
||||
.unwrap();
|
||||
}
|
||||
tx.send(Event::DownloadDone(id, download.id)).unwrap();
|
||||
}
|
||||
});
|
||||
Worker { id, tx: worker_tx }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn downloads() -> Downloads {
|
||||
let distribution = Uniform::new(0, 1000);
|
||||
let mut rng = rand::thread_rng();
|
||||
let pending = (0..NUM_DOWNLOADS)
|
||||
.map(|id| {
|
||||
let size = distribution.sample(&mut rng);
|
||||
Download { id, size }
|
||||
})
|
||||
.collect();
|
||||
Downloads {
|
||||
pending,
|
||||
in_progress: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
workers: Vec<Worker>,
|
||||
mut downloads: Downloads,
|
||||
rx: mpsc::Receiver<Event>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let mut redraw = true;
|
||||
loop {
|
||||
if redraw {
|
||||
terminal.draw(|f| ui(f, &downloads))?;
|
||||
}
|
||||
redraw = true;
|
||||
|
||||
let span = span!(Level::INFO, "recv");
|
||||
let _guard = span.enter();
|
||||
match rx.recv()? {
|
||||
Event::Input(event) => {
|
||||
if event.code == crossterm::event::KeyCode::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Resize => {
|
||||
event!(Level::INFO, "resize");
|
||||
terminal.resize()?;
|
||||
}
|
||||
Event::Tick => {
|
||||
event!(Level::INFO, "tick");
|
||||
}
|
||||
Event::DownloadUpdate(worker_id, download_id, progress) => {
|
||||
event!(
|
||||
Level::INFO,
|
||||
worker_id,
|
||||
download_id,
|
||||
progress,
|
||||
"download update"
|
||||
);
|
||||
let download = downloads.in_progress.get_mut(&worker_id).unwrap();
|
||||
download.progress = progress;
|
||||
redraw = false
|
||||
}
|
||||
Event::DownloadDone(worker_id, download_id) => {
|
||||
event!(Level::INFO, worker_id, download_id, "download done");
|
||||
let download = downloads.in_progress.remove(&worker_id).unwrap();
|
||||
terminal.insert_before(1, |buf| {
|
||||
Paragraph::new(Spans::from(vec![
|
||||
Span::from("Finished "),
|
||||
Span::styled(
|
||||
format!("download {}", download_id),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::from(format!(
|
||||
" in {}ms",
|
||||
download.started_at.elapsed().as_millis()
|
||||
)),
|
||||
]))
|
||||
.render(buf.area, buf);
|
||||
})?;
|
||||
match downloads.next(worker_id) {
|
||||
Some(d) => workers[worker_id].tx.send(d).unwrap(),
|
||||
None => {
|
||||
if downloads.in_progress.is_empty() {
|
||||
terminal.insert_before(1, |buf| {
|
||||
Paragraph::new("Done !").render(buf.area, buf);
|
||||
})?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, downloads: &Downloads) {
|
||||
let size = f.size();
|
||||
|
||||
let block = Block::default()
|
||||
.title("Progress")
|
||||
.title_alignment(Alignment::Center);
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.constraints(vec![Constraint::Length(2), Constraint::Length(4)])
|
||||
.margin(1)
|
||||
.split(size);
|
||||
|
||||
// total progress
|
||||
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
|
||||
let progress = LineGauge::default()
|
||||
.gauge_style(Style::default().fg(Color::Blue))
|
||||
.label(format!("{}/{}", done, NUM_DOWNLOADS))
|
||||
.ratio(done as f64 / NUM_DOWNLOADS as f64);
|
||||
f.render_widget(progress, chunks[0]);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Percentage(20), Constraint::Percentage(80)])
|
||||
.split(chunks[1]);
|
||||
|
||||
// in progress downloads
|
||||
let items: Vec<ListItem> = downloads
|
||||
.in_progress
|
||||
.iter()
|
||||
.map(|(_worker_id, download)| {
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::raw(symbols::DOT),
|
||||
Span::styled(
|
||||
format!(" download {:>2}", download.id),
|
||||
Style::default()
|
||||
.fg(Color::LightGreen)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(format!(
|
||||
" ({}ms)",
|
||||
download.started_at.elapsed().as_millis()
|
||||
)),
|
||||
]))
|
||||
})
|
||||
.collect();
|
||||
let list = List::new(items);
|
||||
f.render_widget(list, chunks[0]);
|
||||
|
||||
for (i, (_, download)) in downloads.in_progress.iter().enumerate() {
|
||||
let gauge = Gauge::default()
|
||||
.gauge_style(Style::default().fg(Color::Yellow))
|
||||
.ratio(download.progress / 100.0);
|
||||
if chunks[1].top().saturating_add(i as u16) > size.bottom() {
|
||||
continue;
|
||||
}
|
||||
f.render_widget(
|
||||
gauge,
|
||||
Rect {
|
||||
x: chunks[1].left(),
|
||||
y: chunks[1].top().saturating_add(i as u16),
|
||||
width: chunks[1].width,
|
||||
height: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,70 @@
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
widgets::{Block, Borders},
|
||||
Terminal,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
fn main() -> Result<(), Box<dyn 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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let events = Events::new();
|
||||
// create app and run it
|
||||
let res = run_app(&mut terminal);
|
||||
|
||||
loop {
|
||||
terminal.draw(|f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(80),
|
||||
Constraint::Percentage(10),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
let block = Block::default().title("Block").borders(Borders::ALL);
|
||||
f.render_widget(block, chunks[0]);
|
||||
let block = Block::default().title("Block 2").borders(Borders::ALL);
|
||||
f.render_widget(block, chunks[2]);
|
||||
})?;
|
||||
|
||||
if let Event::Input(input) = events.next()? {
|
||||
if let Key::Char('q') = input {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(80),
|
||||
Constraint::Percentage(10),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
|
||||
let block = Block::default().title("Block").borders(Borders::ALL);
|
||||
f.render_widget(block, chunks[0]);
|
||||
let block = Block::default().title("Block 2").borders(Borders::ALL);
|
||||
f.render_widget(block, chunks[2]);
|
||||
}
|
||||
|
||||
319
examples/list.rs
319
examples/list.rs
@@ -1,21 +1,68 @@
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::{
|
||||
event::{Event, Events},
|
||||
StatefulList,
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Corner, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, List, ListItem},
|
||||
Terminal,
|
||||
widgets::{Block, Borders, List, ListItem, ListState},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct StatefulList<T> {
|
||||
state: ListState,
|
||||
items: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T> StatefulList<T> {
|
||||
fn with_items(items: Vec<T>) -> StatefulList<T> {
|
||||
StatefulList {
|
||||
state: ListState::default(),
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
fn unselect(&mut self) {
|
||||
self.state.select(None);
|
||||
}
|
||||
}
|
||||
|
||||
/// This struct holds the current state of the app. In particular, it has the `items` field which is a wrapper
|
||||
/// around `ListState`. Keeping track of the items state let us render the associated widget with its state
|
||||
/// and have access to features such as natural scrolling.
|
||||
@@ -90,134 +137,150 @@ impl<'a> App<'a> {
|
||||
|
||||
/// Rotate through the event list.
|
||||
/// This only exists to simulate some kind of "progress"
|
||||
fn advance(&mut self) {
|
||||
fn on_tick(&mut self) {
|
||||
let event = self.events.remove(0);
|
||||
self.events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn 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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let events = Events::new();
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// Create a new app with some example state
|
||||
let mut app = App::new();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
loop {
|
||||
terminal.draw(|f| {
|
||||
// Create two chunks with equal horizontal screen space
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
// Iterate through all elements in the `items` app and append some debug text to it.
|
||||
let items: Vec<ListItem> = app
|
||||
.items
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let mut lines = vec![Spans::from(i.0)];
|
||||
for _ in 0..i.1 {
|
||||
lines.push(Spans::from(Span::styled(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
Style::default().add_modifier(Modifier::ITALIC),
|
||||
)));
|
||||
}
|
||||
ListItem::new(lines).style(Style::default().fg(Color::Black).bg(Color::White))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Create a List from all list items and highlight the currently selected one
|
||||
let items = List::new(items)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(Color::LightGreen)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_symbol(">> ");
|
||||
|
||||
// We can now render the item list
|
||||
f.render_stateful_widget(items, chunks[0], &mut app.items.state);
|
||||
|
||||
// Let's do the same for the events.
|
||||
// The event list doesn't have any state and only displays the current state of the list.
|
||||
let events: Vec<ListItem> = app
|
||||
.events
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|&(event, level)| {
|
||||
// Colorcode the level depending on its type
|
||||
let s = match level {
|
||||
"CRITICAL" => Style::default().fg(Color::Red),
|
||||
"ERROR" => Style::default().fg(Color::Magenta),
|
||||
"WARNING" => Style::default().fg(Color::Yellow),
|
||||
"INFO" => Style::default().fg(Color::Blue),
|
||||
_ => Style::default(),
|
||||
};
|
||||
// Add a example datetime and apply proper spacing between them
|
||||
let header = Spans::from(vec![
|
||||
Span::styled(format!("{:<9}", level), s),
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
"2020-01-01 10:00:00",
|
||||
Style::default().add_modifier(Modifier::ITALIC),
|
||||
),
|
||||
]);
|
||||
// The event gets its own line
|
||||
let log = Spans::from(vec![Span::raw(event)]);
|
||||
|
||||
// Here several things happen:
|
||||
// 1. Add a `---` spacing line above the final list entry
|
||||
// 2. Add the Level + datetime
|
||||
// 3. Add a spacer line
|
||||
// 4. Add the actual event
|
||||
ListItem::new(vec![
|
||||
Spans::from("-".repeat(chunks[1].width as usize)),
|
||||
header,
|
||||
Spans::from(""),
|
||||
log,
|
||||
])
|
||||
})
|
||||
.collect();
|
||||
let events_list = List::new(events)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.start_corner(Corner::BottomLeft);
|
||||
f.render_widget(events_list, chunks[1]);
|
||||
})?;
|
||||
|
||||
// This is a simple example on how to handle events
|
||||
// 1. This breaks the loop and exits the program on `q` button press.
|
||||
// 2. The `up`/`down` keys change the currently selected item in the App's `items` list.
|
||||
// 3. `left` unselects the current item.
|
||||
match events.next()? {
|
||||
Event::Input(input) => match input {
|
||||
Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
Key::Left => {
|
||||
app.items.unselect();
|
||||
}
|
||||
Key::Down => {
|
||||
app.items.next();
|
||||
}
|
||||
Key::Up => {
|
||||
app.items.previous();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Event::Tick => {
|
||||
app.advance();
|
||||
}
|
||||
}
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Left => app.items.unselect(),
|
||||
KeyCode::Down => app.items.next(),
|
||||
KeyCode::Up => app.items.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
// Create two chunks with equal horizontal screen space
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
// Iterate through all elements in the `items` app and append some debug text to it.
|
||||
let items: Vec<ListItem> = app
|
||||
.items
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let mut lines = vec![Spans::from(i.0)];
|
||||
for _ in 0..i.1 {
|
||||
lines.push(Spans::from(Span::styled(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
Style::default().add_modifier(Modifier::ITALIC),
|
||||
)));
|
||||
}
|
||||
ListItem::new(lines).style(Style::default().fg(Color::Black).bg(Color::White))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Create a List from all list items and highlight the currently selected one
|
||||
let items = List::new(items)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(Color::LightGreen)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_symbol(">> ");
|
||||
|
||||
// We can now render the item list
|
||||
f.render_stateful_widget(items, chunks[0], &mut app.items.state);
|
||||
|
||||
// Let's do the same for the events.
|
||||
// The event list doesn't have any state and only displays the current state of the list.
|
||||
let events: Vec<ListItem> = app
|
||||
.events
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|&(event, level)| {
|
||||
// Colorcode the level depending on its type
|
||||
let s = match level {
|
||||
"CRITICAL" => Style::default().fg(Color::Red),
|
||||
"ERROR" => Style::default().fg(Color::Magenta),
|
||||
"WARNING" => Style::default().fg(Color::Yellow),
|
||||
"INFO" => Style::default().fg(Color::Blue),
|
||||
_ => Style::default(),
|
||||
};
|
||||
// Add a example datetime and apply proper spacing between them
|
||||
let header = Spans::from(vec![
|
||||
Span::styled(format!("{:<9}", level), s),
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
"2020-01-01 10:00:00",
|
||||
Style::default().add_modifier(Modifier::ITALIC),
|
||||
),
|
||||
]);
|
||||
// The event gets its own line
|
||||
let log = Spans::from(vec![Span::raw(event)]);
|
||||
|
||||
// Here several things happen:
|
||||
// 1. Add a `---` spacing line above the final list entry
|
||||
// 2. Add the Level + datetime
|
||||
// 3. Add a spacer line
|
||||
// 4. Add the actual event
|
||||
ListItem::new(vec![
|
||||
Spans::from("-".repeat(chunks[1].width as usize)),
|
||||
header,
|
||||
Spans::from(""),
|
||||
log,
|
||||
])
|
||||
})
|
||||
.collect();
|
||||
let events_list = List::new(events)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.start_corner(Corner::BottomLeft);
|
||||
f.render_widget(events_list, chunks[1]);
|
||||
}
|
||||
|
||||
@@ -1,111 +1,171 @@
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
Terminal,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App {
|
||||
scroll: u16,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App { scroll: 0 }
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
self.scroll += 1;
|
||||
self.scroll %= 10;
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn 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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let events = Events::new();
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
let mut scroll: u16 = 0;
|
||||
loop {
|
||||
terminal.draw(|f| {
|
||||
let size = f.size();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
// 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');
|
||||
|
||||
let block = Block::default()
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black));
|
||||
f.render_widget(block, 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 = vec![
|
||||
Spans::from("This is a line "),
|
||||
Spans::from(Span::styled("This is a line ", Style::default().fg(Color::Red))),
|
||||
Spans::from(Span::styled("This is a line", Style::default().bg(Color::Blue))),
|
||||
Spans::from(Span::styled(
|
||||
"This is a longer line",
|
||||
Style::default().add_modifier(Modifier::CROSSED_OUT),
|
||||
)),
|
||||
Spans::from(Span::styled(&long_line, Style::default().bg(Color::Green))),
|
||||
Spans::from(Span::styled(
|
||||
"This is a line",
|
||||
Style::default().fg(Color::Green).add_modifier(Modifier::ITALIC),
|
||||
)),
|
||||
];
|
||||
|
||||
let create_block = |title| {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.title(Span::styled(title, Style::default().add_modifier(Modifier::BOLD)))
|
||||
};
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.block(create_block("Left, no wrap"))
|
||||
.alignment(Alignment::Left);
|
||||
f.render_widget(paragraph, chunks[0]);
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.block(create_block("Left, wrap"))
|
||||
.alignment(Alignment::Left)
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[1]);
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.block(create_block("Center, wrap"))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true })
|
||||
.scroll((scroll, 0));
|
||||
f.render_widget(paragraph, chunks[2]);
|
||||
let paragraph = Paragraph::new(text)
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.block(create_block("Right, wrap"))
|
||||
.alignment(Alignment::Right)
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[3]);
|
||||
})?;
|
||||
|
||||
scroll += 1;
|
||||
scroll %= 10;
|
||||
|
||||
if let Event::Input(key) = events.next()? {
|
||||
if key == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
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');
|
||||
|
||||
let block = Block::default().style(Style::default().bg(Color::White).fg(Color::Black));
|
||||
f.render_widget(block, 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 = vec![
|
||||
Spans::from("This is a line "),
|
||||
Spans::from(Span::styled(
|
||||
"This is a line ",
|
||||
Style::default().fg(Color::Red),
|
||||
)),
|
||||
Spans::from(Span::styled(
|
||||
"This is a line",
|
||||
Style::default().bg(Color::Blue),
|
||||
)),
|
||||
Spans::from(Span::styled(
|
||||
"This is a longer line",
|
||||
Style::default().add_modifier(Modifier::CROSSED_OUT),
|
||||
)),
|
||||
Spans::from(Span::styled(&long_line, Style::default().bg(Color::Green))),
|
||||
Spans::from(Span::styled(
|
||||
"This is a line",
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
)),
|
||||
];
|
||||
|
||||
let create_block = |title| {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.title(Span::styled(
|
||||
title,
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
))
|
||||
};
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.block(create_block("Left, no wrap"))
|
||||
.alignment(Alignment::Left);
|
||||
f.render_widget(paragraph, chunks[0]);
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.block(create_block("Left, wrap"))
|
||||
.alignment(Alignment::Left)
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[1]);
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.block(create_block("Center, wrap"))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true })
|
||||
.scroll((app.scroll, 0));
|
||||
f.render_widget(paragraph, chunks[2]);
|
||||
let paragraph = Paragraph::new(text)
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.block(create_block("Right, wrap"))
|
||||
.alignment(Alignment::Right)
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[3]);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,106 @@
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
text::Span,
|
||||
widgets::{Block, Borders, Clear, Paragraph, Wrap},
|
||||
Terminal,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
/// helper function to create a centered rect using up
|
||||
/// certain percentage of the available rect `r`
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
|
||||
struct App {
|
||||
show_popup: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App { show_popup: false }
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Char('p') => app.show_popup = !app.show_popup,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let size = f.size();
|
||||
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(20), Constraint::Percentage(80)].as_ref())
|
||||
.split(size);
|
||||
|
||||
let text = if app.show_popup {
|
||||
"Press p to close the popup"
|
||||
} else {
|
||||
"Press p to show the popup"
|
||||
};
|
||||
let paragraph = Paragraph::new(Span::styled(
|
||||
text,
|
||||
Style::default().add_modifier(Modifier::SLOW_BLINK),
|
||||
))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[0]);
|
||||
|
||||
let block = Block::default()
|
||||
.title("Content")
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().bg(Color::Blue));
|
||||
f.render_widget(block, chunks[1]);
|
||||
|
||||
if app.show_popup {
|
||||
let block = Block::default().title("Popup").borders(Borders::ALL);
|
||||
let area = centered_rect(60, 20, size);
|
||||
f.render_widget(Clear, area); //this clears out the background
|
||||
f.render_widget(block, area);
|
||||
}
|
||||
}
|
||||
|
||||
/// helper function to create a centered rect using up certain percentage of the available rect `r`
|
||||
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
||||
let popup_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
@@ -40,67 +126,3 @@ fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
||||
)
|
||||
.split(popup_layout[1])[1]
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn 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(|f| {
|
||||
let size = f.size();
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(size);
|
||||
|
||||
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
|
||||
let mut long_line = s.repeat(usize::from(size.width)*usize::from(size.height)/300);
|
||||
long_line.push('\n');
|
||||
|
||||
let text = vec![
|
||||
Spans::from("This is a line "),
|
||||
Spans::from(Span::styled("This is a line ", Style::default().fg(Color::Red))),
|
||||
Spans::from(Span::styled("This is a line", Style::default().bg(Color::Blue))),
|
||||
Spans::from(Span::styled(
|
||||
"This is a longer line\n",
|
||||
Style::default().add_modifier(Modifier::CROSSED_OUT),
|
||||
)),
|
||||
Spans::from(Span::styled(&long_line, Style::default().bg(Color::Green))),
|
||||
Spans::from(Span::styled(
|
||||
"This is a line\n",
|
||||
Style::default().fg(Color::Green).add_modifier(Modifier::ITALIC),
|
||||
)),
|
||||
];
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.block(Block::default().title("Left Block").borders(Borders::ALL))
|
||||
.alignment(Alignment::Left).wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[0]);
|
||||
|
||||
let paragraph = Paragraph::new(text)
|
||||
.block(Block::default().title("Right Block").borders(Borders::ALL))
|
||||
.alignment(Alignment::Left).wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[1]);
|
||||
|
||||
let block = Block::default().title("Popup").borders(Borders::ALL);
|
||||
let area = centered_rect(60, 20, size);
|
||||
f.render_widget(Clear, area); //this clears out the background
|
||||
f.render_widget(block, area);
|
||||
})?;
|
||||
|
||||
if let Event::Input(input) = events.next()? {
|
||||
if let Key::Char('q') = input {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
mod demo;
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::demo::{ui, App};
|
||||
use argh::FromArgs;
|
||||
use rustbox::keyboard::Key;
|
||||
use std::{
|
||||
error::Error,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tui::{backend::RustboxBackend, Terminal};
|
||||
|
||||
/// Rustbox demo
|
||||
#[derive(Debug, FromArgs)]
|
||||
struct Cli {
|
||||
/// time in ms between two ticks.
|
||||
#[argh(option, default = "250")]
|
||||
tick_rate: u64,
|
||||
/// whether unicode symbols are used to improve the overall look of the app
|
||||
#[argh(option, default = "true")]
|
||||
enhanced_graphics: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let cli: Cli = argh::from_env();
|
||||
|
||||
let backend = RustboxBackend::new()?;
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let mut app = App::new("Rustbox demo", cli.enhanced_graphics);
|
||||
|
||||
let mut last_tick = Instant::now();
|
||||
let tick_rate = Duration::from_millis(cli.tick_rate);
|
||||
loop {
|
||||
terminal.draw(|f| ui::draw(f, &mut app))?;
|
||||
if let Ok(rustbox::Event::KeyEvent(key)) =
|
||||
terminal.backend().rustbox().peek_event(tick_rate, false)
|
||||
{
|
||||
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,20 +1,47 @@
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::{
|
||||
event::{Event, Events},
|
||||
RandomSignal,
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Borders, Sparkline},
|
||||
Terminal,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RandomSignal {
|
||||
distribution: Uniform<u64>,
|
||||
rng: ThreadRng,
|
||||
}
|
||||
|
||||
impl RandomSignal {
|
||||
pub fn new(lower: u64, upper: u64) -> RandomSignal {
|
||||
RandomSignal {
|
||||
distribution: Uniform::new(lower, upper),
|
||||
rng: rand::thread_rng(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for RandomSignal {
|
||||
type Item = u64;
|
||||
fn next(&mut self) -> Option<u64> {
|
||||
Some(self.distribution.sample(&mut self.rng))
|
||||
}
|
||||
}
|
||||
|
||||
struct App {
|
||||
signal: RandomSignal,
|
||||
data1: Vec<u64>,
|
||||
@@ -36,7 +63,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
fn on_tick(&mut self) {
|
||||
let value = self.signal.next().unwrap();
|
||||
self.data1.pop();
|
||||
self.data1.insert(0, value);
|
||||
@@ -50,75 +77,100 @@ impl App {
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn 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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// Setup event handlers
|
||||
let events = Events::new();
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// Create default app state
|
||||
let mut app = App::new();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
loop {
|
||||
terminal.draw(|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());
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data1")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data1)
|
||||
.style(Style::default().fg(Color::Yellow));
|
||||
f.render_widget(sparkline, chunks[0]);
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data2")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data2)
|
||||
.style(Style::default().bg(Color::Green));
|
||||
f.render_widget(sparkline, chunks[1]);
|
||||
// Multiline
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data3")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data3)
|
||||
.style(Style::default().fg(Color::Red));
|
||||
f.render_widget(sparkline, chunks[2]);
|
||||
})?;
|
||||
|
||||
match events.next()? {
|
||||
Event::Input(input) => {
|
||||
if input == Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Tick => {
|
||||
app.update();
|
||||
}
|
||||
}
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
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());
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data1")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data1)
|
||||
.style(Style::default().fg(Color::Yellow));
|
||||
f.render_widget(sparkline, chunks[0]);
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data2")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data2)
|
||||
.style(Style::default().bg(Color::Green));
|
||||
f.render_widget(sparkline, chunks[1]);
|
||||
// Multiline
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data3")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data3)
|
||||
.style(Style::default().fg(Color::Red));
|
||||
f.render_widget(sparkline, chunks[2]);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{Block, Borders, Cell, Row, Table, TableState},
|
||||
Terminal,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
pub struct StatefulTable<'a> {
|
||||
struct App<'a> {
|
||||
state: TableState,
|
||||
items: Vec<Vec<&'a str>>,
|
||||
}
|
||||
|
||||
impl<'a> StatefulTable<'a> {
|
||||
fn new() -> StatefulTable<'a> {
|
||||
StatefulTable {
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
state: TableState::default(),
|
||||
items: vec![
|
||||
vec!["Row11", "Row12", "Row13"],
|
||||
@@ -74,72 +74,82 @@ impl<'a> StatefulTable<'a> {
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn 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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let events = Events::new();
|
||||
// create app and run it
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app);
|
||||
|
||||
let mut table = StatefulTable::new();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
// Input
|
||||
loop {
|
||||
terminal.draw(|f| {
|
||||
let rects = Layout::default()
|
||||
.constraints([Constraint::Percentage(100)].as_ref())
|
||||
.margin(5)
|
||||
.split(f.size());
|
||||
|
||||
let selected_style = Style::default().add_modifier(Modifier::REVERSED);
|
||||
let normal_style = Style::default().bg(Color::Blue);
|
||||
let header_cells = ["Header1", "Header2", "Header3"]
|
||||
.iter()
|
||||
.map(|h| Cell::from(*h).style(Style::default().fg(Color::Red)));
|
||||
let header = Row::new(header_cells)
|
||||
.style(normal_style)
|
||||
.height(1)
|
||||
.bottom_margin(1);
|
||||
let rows = table.items.iter().map(|item| {
|
||||
let height = item
|
||||
.iter()
|
||||
.map(|content| content.chars().filter(|c| *c == '\n').count())
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
+ 1;
|
||||
let cells = item.iter().map(|c| Cell::from(*c));
|
||||
Row::new(cells).height(height as u16).bottom_margin(1)
|
||||
});
|
||||
let t = Table::new(rows)
|
||||
.header(header)
|
||||
.block(Block::default().borders(Borders::ALL).title("Table"))
|
||||
.highlight_style(selected_style)
|
||||
.highlight_symbol(">> ")
|
||||
.widths(&[
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Length(30),
|
||||
Constraint::Max(10),
|
||||
]);
|
||||
f.render_stateful_widget(t, rects[0], &mut table.state);
|
||||
})?;
|
||||
|
||||
if let Event::Input(key) = events.next()? {
|
||||
match key {
|
||||
Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
Key::Down => {
|
||||
table.next();
|
||||
}
|
||||
Key::Up => {
|
||||
table.previous();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
};
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Down => app.next(),
|
||||
KeyCode::Up => app.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
let rects = Layout::default()
|
||||
.constraints([Constraint::Percentage(100)].as_ref())
|
||||
.margin(5)
|
||||
.split(f.size());
|
||||
|
||||
let selected_style = Style::default().add_modifier(Modifier::REVERSED);
|
||||
let normal_style = Style::default().bg(Color::Blue);
|
||||
let header_cells = ["Header1", "Header2", "Header3"]
|
||||
.iter()
|
||||
.map(|h| Cell::from(*h).style(Style::default().fg(Color::Red)));
|
||||
let header = Row::new(header_cells)
|
||||
.style(normal_style)
|
||||
.height(1)
|
||||
.bottom_margin(1);
|
||||
let rows = app.items.iter().map(|item| {
|
||||
let height = item
|
||||
.iter()
|
||||
.map(|content| content.chars().filter(|c| *c == '\n').count())
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
+ 1;
|
||||
let cells = item.iter().map(|c| Cell::from(*c));
|
||||
Row::new(cells).height(height as u16).bottom_margin(1)
|
||||
});
|
||||
let t = Table::new(rows)
|
||||
.header(header)
|
||||
.block(Block::default().borders(Borders::ALL).title("Table"))
|
||||
.highlight_style(selected_style)
|
||||
.highlight_symbol(">> ")
|
||||
.widths(&[
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Length(30),
|
||||
Constraint::Min(10),
|
||||
]);
|
||||
f.render_stateful_widget(t, rects[0], &mut app.state);
|
||||
}
|
||||
|
||||
170
examples/tabs.rs
170
examples/tabs.rs
@@ -1,94 +1,124 @@
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::{
|
||||
event::{Event, Events},
|
||||
TabsState,
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, Tabs},
|
||||
Terminal,
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
struct App<'a> {
|
||||
tabs: TabsState<'a>,
|
||||
pub titles: Vec<&'a str>,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
titles: vec!["Tab0", "Tab1", "Tab2", "Tab3"],
|
||||
index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
self.index = (self.index + 1) % self.titles.len();
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
if self.index > 0 {
|
||||
self.index -= 1;
|
||||
} else {
|
||||
self.index = self.titles.len() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn 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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let events = Events::new();
|
||||
// create app and run it
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app);
|
||||
|
||||
// App
|
||||
let mut app = App {
|
||||
tabs: TabsState::new(vec!["Tab0", "Tab1", "Tab2", "Tab3"]),
|
||||
};
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
// Main loop
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|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);
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let block = Block::default().style(Style::default().bg(Color::White).fg(Color::Black));
|
||||
f.render_widget(block, size);
|
||||
let titles = app
|
||||
.tabs
|
||||
.titles
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let (first, rest) = t.split_at(1);
|
||||
Spans::from(vec![
|
||||
Span::styled(first, Style::default().fg(Color::Yellow)),
|
||||
Span::styled(rest, Style::default().fg(Color::Green)),
|
||||
])
|
||||
})
|
||||
.collect();
|
||||
let tabs = Tabs::new(titles)
|
||||
.block(Block::default().borders(Borders::ALL).title("Tabs"))
|
||||
.select(app.tabs.index)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Black),
|
||||
);
|
||||
f.render_widget(tabs, chunks[0]);
|
||||
let inner = match app.tabs.index {
|
||||
0 => Block::default().title("Inner 0").borders(Borders::ALL),
|
||||
1 => Block::default().title("Inner 1").borders(Borders::ALL),
|
||||
2 => Block::default().title("Inner 2").borders(Borders::ALL),
|
||||
3 => Block::default().title("Inner 3").borders(Borders::ALL),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
f.render_widget(inner, chunks[1]);
|
||||
})?;
|
||||
|
||||
if let Event::Input(input) = events.next()? {
|
||||
match input {
|
||||
Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
Key::Right => app.tabs.next(),
|
||||
Key::Left => app.tabs.previous(),
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Right => app.next(),
|
||||
KeyCode::Left => app.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
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 block = Block::default().style(Style::default().bg(Color::White).fg(Color::Black));
|
||||
f.render_widget(block, size);
|
||||
let titles = app
|
||||
.titles
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let (first, rest) = t.split_at(1);
|
||||
Spans::from(vec![
|
||||
Span::styled(first, Style::default().fg(Color::Yellow)),
|
||||
Span::styled(rest, Style::default().fg(Color::Green)),
|
||||
])
|
||||
})
|
||||
.collect();
|
||||
let tabs = Tabs::new(titles)
|
||||
.block(Block::default().borders(Borders::ALL).title("Tabs"))
|
||||
.select(app.index)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Black),
|
||||
);
|
||||
f.render_widget(tabs, chunks[0]);
|
||||
let inner = match app.index {
|
||||
0 => Block::default().title("Inner 0").borders(Borders::ALL),
|
||||
1 => Block::default().title("Inner 1").borders(Borders::ALL),
|
||||
2 => Block::default().title("Inner 2").borders(Borders::ALL),
|
||||
3 => Block::default().title("Inner 3").borders(Borders::ALL),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
f.render_widget(inner, chunks[1]);
|
||||
}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
mod demo;
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::{
|
||||
demo::{ui, App},
|
||||
util::event::{Config, Event, Events},
|
||||
};
|
||||
use argh::FromArgs;
|
||||
use std::{error::Error, io, time::Duration};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{backend::TermionBackend, Terminal};
|
||||
|
||||
/// Termion demo
|
||||
#[derive(Debug, FromArgs)]
|
||||
struct Cli {
|
||||
/// time in ms between two ticks.
|
||||
#[argh(option, default = "250")]
|
||||
tick_rate: u64,
|
||||
/// whether unicode symbols are used to improve the overall look of the app
|
||||
#[argh(option, default = "true")]
|
||||
enhanced_graphics: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let cli: Cli = argh::from_env();
|
||||
|
||||
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)?;
|
||||
|
||||
let mut app = App::new("Termion demo", cli.enhanced_graphics);
|
||||
loop {
|
||||
terminal.draw(|f| ui::draw(f, &mut 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,20 +9,19 @@
|
||||
/// * Pressing Backspace erases a character
|
||||
/// * Pressing Enter pushes the current input in the history of previous
|
||||
/// messages
|
||||
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
|
||||
use crate::util::event::{Event, Events};
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use std::{error::Error, io};
|
||||
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
|
||||
use tui::{
|
||||
backend::TermionBackend,
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans, Text},
|
||||
widgets::{Block, Borders, List, ListItem, Paragraph},
|
||||
Terminal,
|
||||
Frame, Terminal,
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
@@ -52,122 +51,59 @@ impl Default for App {
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn 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);
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// Setup event handlers
|
||||
let events = Events::new();
|
||||
// create app and run it
|
||||
let app = App::default();
|
||||
let res = run_app(&mut terminal, app);
|
||||
|
||||
// Create default app state
|
||||
let mut app = App::default();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{:?}", err)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
|
||||
loop {
|
||||
// Draw UI
|
||||
terminal.draw(|f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let (msg, style) = match app.input_mode {
|
||||
InputMode::Normal => (
|
||||
vec![
|
||||
Span::raw("Press "),
|
||||
Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to exit, "),
|
||||
Span::styled("e", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to start editing."),
|
||||
],
|
||||
Style::default().add_modifier(Modifier::RAPID_BLINK),
|
||||
),
|
||||
InputMode::Editing => (
|
||||
vec![
|
||||
Span::raw("Press "),
|
||||
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to stop editing, "),
|
||||
Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to record the message"),
|
||||
],
|
||||
Style::default(),
|
||||
),
|
||||
};
|
||||
let mut text = Text::from(Spans::from(msg));
|
||||
text.patch_style(style);
|
||||
let help_message = Paragraph::new(text);
|
||||
f.render_widget(help_message, chunks[0]);
|
||||
|
||||
let input = Paragraph::new(app.input.as_ref())
|
||||
.style(match app.input_mode {
|
||||
InputMode::Normal => Style::default(),
|
||||
InputMode::Editing => Style::default().fg(Color::Yellow),
|
||||
})
|
||||
.block(Block::default().borders(Borders::ALL).title("Input"));
|
||||
f.render_widget(input, chunks[1]);
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match app.input_mode {
|
||||
InputMode::Normal =>
|
||||
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
|
||||
{}
|
||||
|
||||
InputMode::Editing => {
|
||||
// Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering
|
||||
f.set_cursor(
|
||||
// Put cursor past the end of the input text
|
||||
chunks[1].x + app.input.width() as u16 + 1,
|
||||
// Move one line down, from the border to the input line
|
||||
chunks[1].y + 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let messages: Vec<ListItem> = app
|
||||
.messages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, m)| {
|
||||
let content = vec![Spans::from(Span::raw(format!("{}: {}", i, m)))];
|
||||
ListItem::new(content)
|
||||
})
|
||||
.collect();
|
||||
let messages =
|
||||
List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages"));
|
||||
f.render_widget(messages, chunks[2]);
|
||||
})?;
|
||||
|
||||
// Handle input
|
||||
if let Event::Input(input) = events.next()? {
|
||||
match app.input_mode {
|
||||
InputMode::Normal => match input {
|
||||
Key::Char('e') => {
|
||||
InputMode::Normal => match key.code {
|
||||
KeyCode::Char('e') => {
|
||||
app.input_mode = InputMode::Editing;
|
||||
}
|
||||
Key::Char('q') => {
|
||||
break;
|
||||
KeyCode::Char('q') => {
|
||||
return Ok(());
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
InputMode::Editing => match input {
|
||||
Key::Char('\n') => {
|
||||
InputMode::Editing => match key.code {
|
||||
KeyCode::Enter => {
|
||||
app.messages.push(app.input.drain(..).collect());
|
||||
}
|
||||
Key::Char(c) => {
|
||||
KeyCode::Char(c) => {
|
||||
app.input.push(c);
|
||||
}
|
||||
Key::Backspace => {
|
||||
KeyCode::Backspace => {
|
||||
app.input.pop();
|
||||
}
|
||||
Key::Esc => {
|
||||
KeyCode::Esc => {
|
||||
app.input_mode = InputMode::Normal;
|
||||
}
|
||||
_ => {}
|
||||
@@ -175,5 +111,82 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
|
||||
let (msg, style) = match app.input_mode {
|
||||
InputMode::Normal => (
|
||||
vec![
|
||||
Span::raw("Press "),
|
||||
Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to exit, "),
|
||||
Span::styled("e", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to start editing."),
|
||||
],
|
||||
Style::default().add_modifier(Modifier::RAPID_BLINK),
|
||||
),
|
||||
InputMode::Editing => (
|
||||
vec![
|
||||
Span::raw("Press "),
|
||||
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to stop editing, "),
|
||||
Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to record the message"),
|
||||
],
|
||||
Style::default(),
|
||||
),
|
||||
};
|
||||
let mut text = Text::from(Spans::from(msg));
|
||||
text.patch_style(style);
|
||||
let help_message = Paragraph::new(text);
|
||||
f.render_widget(help_message, chunks[0]);
|
||||
|
||||
let input = Paragraph::new(app.input.as_ref())
|
||||
.style(match app.input_mode {
|
||||
InputMode::Normal => Style::default(),
|
||||
InputMode::Editing => Style::default().fg(Color::Yellow),
|
||||
})
|
||||
.block(Block::default().borders(Borders::ALL).title("Input"));
|
||||
f.render_widget(input, chunks[1]);
|
||||
match app.input_mode {
|
||||
InputMode::Normal =>
|
||||
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
|
||||
{}
|
||||
|
||||
InputMode::Editing => {
|
||||
// Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering
|
||||
f.set_cursor(
|
||||
// Put cursor past the end of the input text
|
||||
chunks[1].x + app.input.width() as u16 + 1,
|
||||
// Move one line down, from the border to the input line
|
||||
chunks[1].y + 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let messages: Vec<ListItem> = app
|
||||
.messages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, m)| {
|
||||
let content = vec![Spans::from(Span::raw(format!("{}: {}", i, m)))];
|
||||
ListItem::new(content)
|
||||
})
|
||||
.collect();
|
||||
let messages =
|
||||
List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages"));
|
||||
f.render_widget(messages, chunks[2]);
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
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 tick_rate: Duration,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Config {
|
||||
Config {
|
||||
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() {
|
||||
if let Ok(key) = evt {
|
||||
if let Err(err) = tx.send(Event::Input(key)) {
|
||||
eprintln!("{}", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
let tick_handle = {
|
||||
thread::spawn(move || loop {
|
||||
if let Err(err) = tx.send(Event::Tick) {
|
||||
eprintln!("{}", err);
|
||||
break;
|
||||
}
|
||||
thread::sleep(config.tick_rate);
|
||||
})
|
||||
};
|
||||
Events {
|
||||
rx,
|
||||
input_handle,
|
||||
tick_handle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&self) -> Result<Event<Key>, mpsc::RecvError> {
|
||||
self.rx.recv()
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
#[cfg(feature = "termion")]
|
||||
pub mod event;
|
||||
|
||||
use rand::distributions::{Distribution, Uniform};
|
||||
use rand::rngs::ThreadRng;
|
||||
use tui::widgets::ListState;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RandomSignal {
|
||||
distribution: Uniform<u64>,
|
||||
rng: ThreadRng,
|
||||
}
|
||||
|
||||
impl RandomSignal {
|
||||
pub fn new(lower: u64, upper: u64) -> RandomSignal {
|
||||
RandomSignal {
|
||||
distribution: Uniform::new(lower, upper),
|
||||
rng: rand::thread_rng(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for RandomSignal {
|
||||
type Item = u64;
|
||||
fn next(&mut self) -> Option<u64> {
|
||||
Some(self.distribution.sample(&mut self.rng))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SinSignal {
|
||||
x: f64,
|
||||
interval: f64,
|
||||
period: f64,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
impl SinSignal {
|
||||
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
|
||||
SinSignal {
|
||||
x: 0.0,
|
||||
interval,
|
||||
period,
|
||||
scale,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for SinSignal {
|
||||
type Item = (f64, f64);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
|
||||
self.x += self.interval;
|
||||
Some(point)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TabsState<'a> {
|
||||
pub titles: Vec<&'a str>,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
impl<'a> TabsState<'a> {
|
||||
pub fn new(titles: Vec<&'a str>) -> TabsState {
|
||||
TabsState { titles, index: 0 }
|
||||
}
|
||||
pub fn next(&mut self) {
|
||||
self.index = (self.index + 1) % self.titles.len();
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
if self.index > 0 {
|
||||
self.index -= 1;
|
||||
} else {
|
||||
self.index = self.titles.len() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StatefulList<T> {
|
||||
pub state: ListState,
|
||||
pub items: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T> StatefulList<T> {
|
||||
pub fn new() -> StatefulList<T> {
|
||||
StatefulList {
|
||||
state: ListState::default(),
|
||||
items: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
|
||||
StatefulList {
|
||||
state: ListState::default(),
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
pub fn unselect(&mut self) {
|
||||
self.state.select(None);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
backend::Backend,
|
||||
backend::{Backend, ClearType},
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier},
|
||||
@@ -11,7 +11,7 @@ use crossterm::{
|
||||
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
|
||||
SetForegroundColor,
|
||||
},
|
||||
terminal::{self, Clear, ClearType},
|
||||
terminal::{self, Clear},
|
||||
};
|
||||
use std::io::{self, Write};
|
||||
|
||||
@@ -56,7 +56,7 @@ where
|
||||
for (x, y, cell) in content {
|
||||
// Move the cursor if the previous location was not (x - 1, y)
|
||||
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
|
||||
map_error(queue!(self.buffer, MoveTo(x, y)))?;
|
||||
queue!(self.buffer, MoveTo(x, y))?;
|
||||
}
|
||||
last_pos = Some((x, y));
|
||||
if cell.modifier != modifier {
|
||||
@@ -69,45 +69,60 @@ where
|
||||
}
|
||||
if cell.fg != fg {
|
||||
let color = CColor::from(cell.fg);
|
||||
map_error(queue!(self.buffer, SetForegroundColor(color)))?;
|
||||
queue!(self.buffer, SetForegroundColor(color))?;
|
||||
fg = cell.fg;
|
||||
}
|
||||
if cell.bg != bg {
|
||||
let color = CColor::from(cell.bg);
|
||||
map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
|
||||
queue!(self.buffer, SetBackgroundColor(color))?;
|
||||
bg = cell.bg;
|
||||
}
|
||||
|
||||
map_error(queue!(self.buffer, Print(&cell.symbol)))?;
|
||||
queue!(self.buffer, Print(&cell.symbol))?;
|
||||
}
|
||||
|
||||
map_error(queue!(
|
||||
queue!(
|
||||
self.buffer,
|
||||
SetForegroundColor(CColor::Reset),
|
||||
SetBackgroundColor(CColor::Reset),
|
||||
SetAttribute(CAttribute::Reset)
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
map_error(execute!(self.buffer, Hide))
|
||||
execute!(self.buffer, Hide)
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
map_error(execute!(self.buffer, Show))
|
||||
execute!(self.buffer, Show)
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
crossterm::cursor::position()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
map_error(execute!(self.buffer, MoveTo(x, y)))
|
||||
execute!(self.buffer, MoveTo(x, y))
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
map_error(execute!(self.buffer, Clear(ClearType::All)))
|
||||
fn clear(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
execute!(
|
||||
self.buffer,
|
||||
Clear(match clear_type {
|
||||
ClearType::All => crossterm::terminal::ClearType::All,
|
||||
ClearType::AfterCursor => crossterm::terminal::ClearType::FromCursorDown,
|
||||
ClearType::BeforeCursor => crossterm::terminal::ClearType::FromCursorUp,
|
||||
ClearType::CurrentLine => crossterm::terminal::ClearType::CurrentLine,
|
||||
ClearType::UntilNewLine => crossterm::terminal::ClearType::UntilNewLine,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||
for _ in 0..n {
|
||||
queue!(self.buffer, Print("\n"))?;
|
||||
}
|
||||
self.buffer.flush()
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
@@ -122,10 +137,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn map_error(error: crossterm::Result<()>) -> io::Result<()> {
|
||||
error.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||
}
|
||||
|
||||
impl From<Color> for CColor {
|
||||
fn from(color: Color) -> Self {
|
||||
match color {
|
||||
@@ -166,54 +177,54 @@ impl ModifierDiff {
|
||||
//use crossterm::Attribute;
|
||||
let removed = self.from - self.to;
|
||||
if removed.contains(Modifier::REVERSED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?;
|
||||
queue!(w, SetAttribute(CAttribute::NoReverse))?;
|
||||
}
|
||||
if removed.contains(Modifier::BOLD) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
|
||||
queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
|
||||
if self.to.contains(Modifier::DIM) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
|
||||
queue!(w, SetAttribute(CAttribute::Dim))?;
|
||||
}
|
||||
}
|
||||
if removed.contains(Modifier::ITALIC) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
|
||||
queue!(w, SetAttribute(CAttribute::NoItalic))?;
|
||||
}
|
||||
if removed.contains(Modifier::UNDERLINED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
|
||||
queue!(w, SetAttribute(CAttribute::NoUnderline))?;
|
||||
}
|
||||
if removed.contains(Modifier::DIM) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
|
||||
queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
|
||||
}
|
||||
if removed.contains(Modifier::CROSSED_OUT) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NotCrossedOut)))?;
|
||||
queue!(w, SetAttribute(CAttribute::NotCrossedOut))?;
|
||||
}
|
||||
if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?;
|
||||
queue!(w, SetAttribute(CAttribute::NoBlink))?;
|
||||
}
|
||||
|
||||
let added = self.to - self.from;
|
||||
if added.contains(Modifier::REVERSED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?;
|
||||
queue!(w, SetAttribute(CAttribute::Reverse))?;
|
||||
}
|
||||
if added.contains(Modifier::BOLD) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Bold)))?;
|
||||
queue!(w, SetAttribute(CAttribute::Bold))?;
|
||||
}
|
||||
if added.contains(Modifier::ITALIC) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Italic)))?;
|
||||
queue!(w, SetAttribute(CAttribute::Italic))?;
|
||||
}
|
||||
if added.contains(Modifier::UNDERLINED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
|
||||
queue!(w, SetAttribute(CAttribute::Underlined))?;
|
||||
}
|
||||
if added.contains(Modifier::DIM) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
|
||||
queue!(w, SetAttribute(CAttribute::Dim))?;
|
||||
}
|
||||
if added.contains(Modifier::CROSSED_OUT) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::CrossedOut)))?;
|
||||
queue!(w, SetAttribute(CAttribute::CrossedOut))?;
|
||||
}
|
||||
if added.contains(Modifier::SLOW_BLINK) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::SlowBlink)))?;
|
||||
queue!(w, SetAttribute(CAttribute::SlowBlink))?;
|
||||
}
|
||||
if added.contains(Modifier::RAPID_BLINK) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::RapidBlink)))?;
|
||||
queue!(w, SetAttribute(CAttribute::RapidBlink))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
use std::io;
|
||||
|
||||
use crate::backend::Backend;
|
||||
use crate::buffer::Cell;
|
||||
use crate::layout::Rect;
|
||||
use crate::style::{Color, Modifier};
|
||||
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 fg = Color::Reset;
|
||||
let mut bg = Color::Reset;
|
||||
let mut 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 {
|
||||
if row != last_row || col != last_col + 1 {
|
||||
self.curses.move_rc(i32::from(row), i32::from(col));
|
||||
}
|
||||
last_col = col;
|
||||
last_row = row;
|
||||
if cell.modifier != modifier {
|
||||
apply_modifier_diff(&mut self.curses.win, modifier, cell.modifier);
|
||||
modifier = cell.modifier;
|
||||
};
|
||||
if cell.fg != fg {
|
||||
update_color = true;
|
||||
if let Some(ccolor) = cell.fg.into() {
|
||||
fg = cell.fg;
|
||||
curses_style.fg = ccolor;
|
||||
} else {
|
||||
fg = Color::White;
|
||||
curses_style.fg = easycurses::Color::White;
|
||||
}
|
||||
};
|
||||
if cell.bg != bg {
|
||||
update_color = true;
|
||||
if let Some(ccolor) = cell.bg.into() {
|
||||
bg = cell.bg;
|
||||
curses_style.bg = ccolor;
|
||||
} else {
|
||||
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 (y, x) = 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(i32::from(y), i32::from(x));
|
||||
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_QUARTERS => pancurses::ACS_BLOCK(),
|
||||
block::FIVE_EIGHTHS => pancurses::ACS_BLOCK(),
|
||||
block::HALF => pancurses::ACS_BLOCK(),
|
||||
block::THREE_EIGHTHS => ' ' as chtype,
|
||||
block::ONE_QUARTER => ' ' as chtype,
|
||||
block::ONE_EIGHTH => ' ' as chtype,
|
||||
bar::SEVEN_EIGHTHS => pancurses::ACS_BLOCK(),
|
||||
bar::THREE_QUARTERS => pancurses::ACS_BLOCK(),
|
||||
bar::FIVE_EIGHTHS => pancurses::ACS_BLOCK(),
|
||||
bar::HALF => pancurses::ACS_BLOCK(),
|
||||
bar::THREE_EIGHTHS => pancurses::ACS_S9(),
|
||||
bar::ONE_QUARTER => 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_QUARTERS => block::FULL,
|
||||
block::FIVE_EIGHTHS => block::HALF,
|
||||
block::THREE_EIGHTHS => block::HALF,
|
||||
block::ONE_QUARTER => block::HALF,
|
||||
block::ONE_EIGHTH => " ",
|
||||
bar::SEVEN_EIGHTHS => bar::FULL,
|
||||
bar::THREE_QUARTERS => bar::FULL,
|
||||
bar::FIVE_EIGHTHS => bar::HALF,
|
||||
bar::THREE_EIGHTHS => bar::HALF,
|
||||
bar::ONE_QUARTER => 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);
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,6 @@ use std::io;
|
||||
use crate::buffer::Cell;
|
||||
use crate::layout::Rect;
|
||||
|
||||
#[cfg(feature = "rustbox")]
|
||||
mod rustbox;
|
||||
#[cfg(feature = "rustbox")]
|
||||
pub use self::rustbox::RustboxBackend;
|
||||
|
||||
#[cfg(feature = "termion")]
|
||||
mod termion;
|
||||
#[cfg(feature = "termion")]
|
||||
@@ -18,23 +13,28 @@ 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;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum ClearType {
|
||||
All,
|
||||
AfterCursor,
|
||||
BeforeCursor,
|
||||
CurrentLine,
|
||||
UntilNewLine,
|
||||
}
|
||||
|
||||
pub trait Backend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>;
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()>;
|
||||
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 clear(&mut self, clear_type: ClearType) -> Result<(), io::Error>;
|
||||
fn size(&self) -> Result<Rect, io::Error>;
|
||||
fn flush(&mut self) -> Result<(), io::Error>;
|
||||
}
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
use crate::{
|
||||
backend::Backend,
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier},
|
||||
};
|
||||
use std::io;
|
||||
|
||||
pub struct RustboxBackend {
|
||||
rustbox: rustbox::RustBox,
|
||||
}
|
||||
|
||||
impl RustboxBackend {
|
||||
pub fn new() -> Result<RustboxBackend, rustbox::InitError> {
|
||||
let rustbox = rustbox::RustBox::init(Default::default())?;
|
||||
Ok(RustboxBackend { rustbox })
|
||||
}
|
||||
|
||||
pub fn with_rustbox(instance: rustbox::RustBox) -> RustboxBackend {
|
||||
RustboxBackend { rustbox: instance }
|
||||
}
|
||||
|
||||
pub fn rustbox(&self) -> &rustbox::RustBox {
|
||||
&self.rustbox
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for RustboxBackend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
for (x, y, cell) in content {
|
||||
self.rustbox.print(
|
||||
x as usize,
|
||||
y as usize,
|
||||
cell.modifier.into(),
|
||||
cell.fg.into(),
|
||||
cell.bg.into(),
|
||||
&cell.symbol,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
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> {
|
||||
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();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn rgb_to_byte(r: u8, g: u8, b: u8) -> u16 {
|
||||
u16::from((r & 0xC0) + ((g & 0xE0) >> 2) + ((b & 0xE0) >> 5))
|
||||
}
|
||||
|
||||
impl From<Color> for rustbox::Color {
|
||||
fn from(color: Color) -> rustbox::Color {
|
||||
match color {
|
||||
Color::Reset => rustbox::Color::Default,
|
||||
Color::Black | Color::Gray | Color::DarkGray => rustbox::Color::Black,
|
||||
Color::Red | Color::LightRed => rustbox::Color::Red,
|
||||
Color::Green | Color::LightGreen => rustbox::Color::Green,
|
||||
Color::Yellow | Color::LightYellow => rustbox::Color::Yellow,
|
||||
Color::Magenta | Color::LightMagenta => rustbox::Color::Magenta,
|
||||
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(u16::from(i)),
|
||||
Color::Rgb(r, g, b) => rustbox::Color::Byte(rgb_to_byte(r, g, b)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Modifier> for rustbox::Style {
|
||||
fn from(modifier: Modifier) -> rustbox::Style {
|
||||
let mut result = rustbox::Style::empty();
|
||||
if modifier.contains(Modifier::BOLD) {
|
||||
result.insert(rustbox::RB_BOLD);
|
||||
}
|
||||
if modifier.contains(Modifier::UNDERLINED) {
|
||||
result.insert(rustbox::RB_UNDERLINE);
|
||||
}
|
||||
if modifier.contains(Modifier::REVERSED) {
|
||||
result.insert(rustbox::RB_REVERSE);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::Backend;
|
||||
use crate::{
|
||||
backend::{Backend, ClearType},
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier},
|
||||
@@ -42,10 +42,21 @@ impl<W> Backend for TermionBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
/// Clears the entire screen and move the cursor to the top left of the screen
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
write!(self.stdout, "{}", termion::clear::All)?;
|
||||
write!(self.stdout, "{}", termion::cursor::Goto(1, 1))?;
|
||||
fn clear(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
match clear_type {
|
||||
ClearType::All => write!(self.stdout, "{}", termion::clear::All)?,
|
||||
ClearType::AfterCursor => write!(self.stdout, "{}", termion::clear::AfterCursor)?,
|
||||
ClearType::BeforeCursor => write!(self.stdout, "{}", termion::clear::BeforeCursor)?,
|
||||
ClearType::CurrentLine => write!(self.stdout, "{}", termion::clear::CurrentLine)?,
|
||||
ClearType::UntilNewLine => write!(self.stdout, "{}", termion::clear::UntilNewline)?,
|
||||
};
|
||||
self.stdout.flush()
|
||||
}
|
||||
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||
for _ in 0..n {
|
||||
writeln!(self.stdout)?;
|
||||
}
|
||||
self.stdout.flush()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
backend::Backend,
|
||||
backend::{Backend, ClearType},
|
||||
buffer::{Buffer, Cell},
|
||||
layout::Rect,
|
||||
};
|
||||
@@ -117,6 +117,10 @@ impl Backend for TestBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn append_lines(&mut self, _n: u16) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||
self.cursor = false;
|
||||
Ok(())
|
||||
@@ -136,7 +140,7 @@ impl Backend for TestBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> Result<(), io::Error> {
|
||||
fn clear(&mut self, _clear_type: ClearType) -> Result<(), io::Error> {
|
||||
self.buffer.reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ impl Default for Cell {
|
||||
/// buf.get_mut(5, 0).set_char('x');
|
||||
/// assert_eq!(buf.get(5, 0).symbol, "x");
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct Buffer {
|
||||
/// The area represented by this buffer
|
||||
pub area: Rect,
|
||||
@@ -114,15 +114,6 @@ pub struct Buffer {
|
||||
pub content: Vec<Cell>,
|
||||
}
|
||||
|
||||
impl Default for Buffer {
|
||||
fn default() -> Buffer {
|
||||
Buffer {
|
||||
area: Default::default(),
|
||||
content: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
/// Returns a Buffer with all cells set to the default one
|
||||
pub fn empty(area: Rect) -> Buffer {
|
||||
@@ -440,7 +431,6 @@ impl Buffer {
|
||||
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:
|
||||
@@ -450,8 +440,7 @@ impl Buffer {
|
||||
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;
|
||||
let (x, y) = self.pos_of(i);
|
||||
updates.push((x, y, &next_buffer[i]));
|
||||
}
|
||||
|
||||
|
||||
@@ -363,7 +363,7 @@ impl Element {
|
||||
|
||||
/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
|
||||
/// area they are supposed to render to.
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
|
||||
pub struct Rect {
|
||||
pub x: u16,
|
||||
pub y: u16,
|
||||
@@ -371,17 +371,6 @@ pub struct Rect {
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
impl Default for Rect {
|
||||
fn default() -> Rect {
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
129
src/lib.rs
129
src/lib.rs
@@ -10,17 +10,17 @@
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! tui = "0.16"
|
||||
//! termion = "1.5"
|
||||
//! crossterm = "0.22"
|
||||
//! ```
|
||||
//!
|
||||
//! The crate is using the `termion` backend by default but if for example you want your
|
||||
//! application to work on Windows, you might want to use the `crossterm` backend instead. This can
|
||||
//! be done by changing your dependencies specification to the following:
|
||||
//! The crate is using the `crossterm` backend by default that works on most platforms. But if for
|
||||
//! example you want to use the `termion` backend instead. This can be done by changing your
|
||||
//! dependencies specification to the following:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! crossterm = "0.20"
|
||||
//! tui = { version = "0.16", default-features = false, features = ['crossterm'] }
|
||||
//! termion = "1.5"
|
||||
//! tui = { version = "0.16", default-features = false, features = ['termion'] }
|
||||
//! ```
|
||||
//!
|
||||
//! The same logic applies for all other available backends.
|
||||
@@ -33,29 +33,27 @@
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io;
|
||||
//! use tui::Terminal;
|
||||
//! use tui::backend::TermionBackend;
|
||||
//! use termion::raw::IntoRawMode;
|
||||
//! use tui::{backend::CrosstermBackend, Terminal};
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! let stdout = io::stdout().into_raw_mode()?;
|
||||
//! let backend = TermionBackend::new(stdout);
|
||||
//! let stdout = io::stdout();
|
||||
//! let backend = CrosstermBackend::new(stdout);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! If you had previously chosen `crossterm` as a backend, the terminal can be created in a similar
|
||||
//! If you had previously chosen `termion` as a backend, the terminal can be created in a similar
|
||||
//! way:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use std::io;
|
||||
//! use tui::Terminal;
|
||||
//! use tui::backend::CrosstermBackend;
|
||||
//! use tui::{backend::TermionBackend, Terminal};
|
||||
//! use termion::raw::IntoRawMode;
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! let stdout = io::stdout();
|
||||
//! let backend = CrosstermBackend::new(stdout);
|
||||
//! let stdout = io::stdout().into_raw_mode()?;
|
||||
//! let backend = TermionBackend::new(stdout);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
@@ -77,17 +75,27 @@
|
||||
//! The following example renders a block of the size of the terminal:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io;
|
||||
//! use termion::raw::IntoRawMode;
|
||||
//! use tui::Terminal;
|
||||
//! use tui::backend::TermionBackend;
|
||||
//! use tui::widgets::{Widget, Block, Borders};
|
||||
//! use tui::layout::{Layout, Constraint, Direction};
|
||||
//! use std::{io, thread, time::Duration};
|
||||
//! use tui::{
|
||||
//! backend::CrosstermBackend,
|
||||
//! widgets::{Widget, Block, Borders},
|
||||
//! layout::{Layout, Constraint, Direction},
|
||||
//! Terminal
|
||||
//! };
|
||||
//! use crossterm::{
|
||||
//! event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
//! execute,
|
||||
//! terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
//! };
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! let stdout = io::stdout().into_raw_mode()?;
|
||||
//! let backend = TermionBackend::new(stdout);
|
||||
//! // setup terminal
|
||||
//! enable_raw_mode()?;
|
||||
//! let mut stdout = io::stdout();
|
||||
//! execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
//! let backend = CrosstermBackend::new(stdout);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//!
|
||||
//! terminal.draw(|f| {
|
||||
//! let size = f.size();
|
||||
//! let block = Block::default()
|
||||
@@ -95,6 +103,18 @@
|
||||
//! .borders(Borders::ALL);
|
||||
//! f.render_widget(block, size);
|
||||
//! })?;
|
||||
//!
|
||||
//! thread::sleep(Duration::from_millis(5000));
|
||||
//!
|
||||
//! // restore terminal
|
||||
//! disable_raw_mode()?;
|
||||
//! execute!(
|
||||
//! terminal.backend_mut(),
|
||||
//! LeaveAlternateScreen,
|
||||
//! DisableMouseCapture
|
||||
//! )?;
|
||||
//! terminal.show_cursor()?;
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
@@ -106,39 +126,32 @@
|
||||
//! full customization. And `Layout` is no exception:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io;
|
||||
//! use termion::raw::IntoRawMode;
|
||||
//! use tui::Terminal;
|
||||
//! use tui::backend::TermionBackend;
|
||||
//! use tui::widgets::{Widget, Block, Borders};
|
||||
//! use tui::layout::{Layout, Constraint, Direction};
|
||||
//!
|
||||
//! 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(|f| {
|
||||
//! let chunks = Layout::default()
|
||||
//! .direction(Direction::Vertical)
|
||||
//! .margin(1)
|
||||
//! .constraints(
|
||||
//! [
|
||||
//! Constraint::Percentage(10),
|
||||
//! Constraint::Percentage(80),
|
||||
//! Constraint::Percentage(10)
|
||||
//! ].as_ref()
|
||||
//! )
|
||||
//! .split(f.size());
|
||||
//! let block = Block::default()
|
||||
//! .title("Block")
|
||||
//! .borders(Borders::ALL);
|
||||
//! f.render_widget(block, chunks[0]);
|
||||
//! let block = Block::default()
|
||||
//! .title("Block 2")
|
||||
//! .borders(Borders::ALL);
|
||||
//! f.render_widget(block, chunks[1]);
|
||||
//! })?;
|
||||
//! Ok(())
|
||||
//! use tui::{
|
||||
//! backend::Backend,
|
||||
//! layout::{Constraint, Direction, Layout},
|
||||
//! widgets::{Block, Borders},
|
||||
//! Frame,
|
||||
//! };
|
||||
//! fn ui<B: Backend>(f: &mut Frame<B>) {
|
||||
//! let chunks = Layout::default()
|
||||
//! .direction(Direction::Vertical)
|
||||
//! .margin(1)
|
||||
//! .constraints(
|
||||
//! [
|
||||
//! Constraint::Percentage(10),
|
||||
//! Constraint::Percentage(80),
|
||||
//! Constraint::Percentage(10)
|
||||
//! ].as_ref()
|
||||
//! )
|
||||
//! .split(f.size());
|
||||
//! let block = Block::default()
|
||||
//! .title("Block")
|
||||
//! .borders(Borders::ALL);
|
||||
//! f.render_widget(block, chunks[0]);
|
||||
//! let block = Block::default()
|
||||
//! .title("Block 2")
|
||||
//! .borders(Borders::ALL);
|
||||
//! f.render_widget(block, chunks[1]);
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
@@ -156,4 +169,4 @@ pub mod terminal;
|
||||
pub mod text;
|
||||
pub mod widgets;
|
||||
|
||||
pub use self::terminal::{Frame, Terminal, TerminalOptions, Viewport};
|
||||
pub use self::terminal::{Frame, Terminal, TerminalOptions, ViewportVariant};
|
||||
|
||||
294
src/terminal.rs
294
src/terminal.rs
@@ -1,40 +1,30 @@
|
||||
use crate::{
|
||||
backend::Backend,
|
||||
backend::{Backend, ClearType},
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
widgets::{StatefulWidget, Widget},
|
||||
};
|
||||
use std::io;
|
||||
use tracing::{event, span, Level};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// UNSTABLE
|
||||
enum ResizeBehavior {
|
||||
Fixed,
|
||||
Auto,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// UNSTABLE
|
||||
pub struct Viewport {
|
||||
area: Rect,
|
||||
resize_behavior: ResizeBehavior,
|
||||
}
|
||||
|
||||
impl Viewport {
|
||||
/// UNSTABLE
|
||||
pub fn fixed(area: Rect) -> Viewport {
|
||||
Viewport {
|
||||
area,
|
||||
resize_behavior: ResizeBehavior::Fixed,
|
||||
}
|
||||
}
|
||||
pub enum ViewportVariant {
|
||||
Fullscreen,
|
||||
Inline(u16),
|
||||
Fixed(Rect),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// Options to pass to [`Terminal::with_options`]
|
||||
pub struct TerminalOptions {
|
||||
/// Viewport used to draw to the terminal
|
||||
pub viewport: Viewport,
|
||||
pub viewport: ViewportVariant,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct Viewport {
|
||||
variant: ViewportVariant,
|
||||
area: Rect,
|
||||
}
|
||||
|
||||
/// Interface to the terminal backed by Termion
|
||||
@@ -53,6 +43,11 @@ where
|
||||
hidden_cursor: bool,
|
||||
/// Viewport
|
||||
viewport: Viewport,
|
||||
/// Last known size of the terminal. Used to detect if the internal buffers have to be resized.
|
||||
last_known_size: Rect,
|
||||
/// Last known position of the cursor. Used to find the new area when the viewport is inlined
|
||||
/// and the terminal resized.
|
||||
last_known_cursor_pos: (u16, u16),
|
||||
}
|
||||
|
||||
/// Represents a consistent terminal interface for rendering.
|
||||
@@ -73,7 +68,7 @@ impl<'a, B> Frame<'a, B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// Terminal size, guaranteed not to change when rendering.
|
||||
/// Frame size, guaranteed not to change when rendering.
|
||||
pub fn size(&self) -> Rect {
|
||||
self.terminal.viewport.area
|
||||
}
|
||||
@@ -82,14 +77,12 @@ where
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::io;
|
||||
/// ```rust
|
||||
/// # use tui::Terminal;
|
||||
/// # use tui::backend::TermionBackend;
|
||||
/// # use tui::backend::TestBackend;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use tui::widgets::Block;
|
||||
/// # let stdout = io::stdout();
|
||||
/// # let backend = TermionBackend::new(stdout);
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// let block = Block::default();
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
@@ -110,14 +103,12 @@ where
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::io;
|
||||
/// ```rust
|
||||
/// # use tui::Terminal;
|
||||
/// # use tui::backend::TermionBackend;
|
||||
/// # use tui::backend::TestBackend;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use tui::widgets::{List, ListItem, ListState};
|
||||
/// # let stdout = io::stdout();
|
||||
/// # let backend = TermionBackend::new(stdout);
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// let mut state = ListState::default();
|
||||
/// state.select(Some(1));
|
||||
@@ -177,29 +168,50 @@ where
|
||||
/// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and
|
||||
/// default colors for the foreground and the background
|
||||
pub fn new(backend: B) -> io::Result<Terminal<B>> {
|
||||
let size = backend.size()?;
|
||||
Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport {
|
||||
area: size,
|
||||
resize_behavior: ResizeBehavior::Auto,
|
||||
},
|
||||
viewport: ViewportVariant::Fullscreen,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// UNSTABLE
|
||||
pub fn with_options(backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
|
||||
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
|
||||
let size = backend.size()?;
|
||||
let (viewport_area, cursor_pos) = match options.viewport {
|
||||
ViewportVariant::Fullscreen => (size, (0, 0)),
|
||||
ViewportVariant::Inline(height) => {
|
||||
let pos = backend.get_cursor()?;
|
||||
let mut row = pos.1;
|
||||
let max_height = size.height.min(height);
|
||||
backend.append_lines(max_height.saturating_sub(1))?;
|
||||
let missing_lines = row.saturating_add(max_height).saturating_sub(size.height);
|
||||
if missing_lines > 0 {
|
||||
row = row.saturating_sub(missing_lines);
|
||||
}
|
||||
(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: row,
|
||||
width: size.width,
|
||||
height: max_height,
|
||||
},
|
||||
pos,
|
||||
)
|
||||
}
|
||||
ViewportVariant::Fixed(area) => (area, (area.left(), area.top())),
|
||||
};
|
||||
Ok(Terminal {
|
||||
backend,
|
||||
buffers: [
|
||||
Buffer::empty(options.viewport.area),
|
||||
Buffer::empty(options.viewport.area),
|
||||
],
|
||||
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
|
||||
current: 0,
|
||||
hidden_cursor: false,
|
||||
viewport: options.viewport,
|
||||
viewport: Viewport {
|
||||
variant: options.viewport,
|
||||
area: viewport_area,
|
||||
},
|
||||
last_known_size: size,
|
||||
last_known_cursor_pos: cursor_pos,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -229,28 +241,61 @@ where
|
||||
let previous_buffer = &self.buffers[1 - self.current];
|
||||
let current_buffer = &self.buffers[self.current];
|
||||
let updates = previous_buffer.diff(current_buffer);
|
||||
if let Some((col, row, _)) = updates.last() {
|
||||
self.last_known_cursor_pos = (*col, *row);
|
||||
}
|
||||
self.backend.draw(updates.into_iter())
|
||||
}
|
||||
|
||||
/// 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].resize(area);
|
||||
self.viewport.area = area;
|
||||
self.clear()
|
||||
/// Queries the backend for size and resizes if it doesn't match the previous size.
|
||||
pub fn resize(&mut self) -> io::Result<()> {
|
||||
let size = self.size()?;
|
||||
if self.last_known_size == size {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
event!(Level::DEBUG, last_known_size = ?self.last_known_size, ?size, "terminal size changed");
|
||||
|
||||
let next_area = match self.viewport.variant {
|
||||
ViewportVariant::Fullscreen => size,
|
||||
ViewportVariant::Inline(height) => {
|
||||
let (_, mut row) = self.get_cursor()?;
|
||||
let offset_in_previous_viewport = self
|
||||
.last_known_cursor_pos
|
||||
.1
|
||||
.saturating_sub(self.viewport.area.top());
|
||||
let max_height = height.min(size.height);
|
||||
let lines_after_cursor = height
|
||||
.saturating_sub(offset_in_previous_viewport)
|
||||
.saturating_sub(1);
|
||||
let available_lines = size.height.saturating_sub(row).saturating_sub(1);
|
||||
let missing_lines = lines_after_cursor.saturating_sub(available_lines);
|
||||
self.backend.append_lines(lines_after_cursor)?;
|
||||
if missing_lines > 0 {
|
||||
row = row.saturating_sub(missing_lines);
|
||||
}
|
||||
row = row.saturating_sub(offset_in_previous_viewport);
|
||||
Rect {
|
||||
x: 0,
|
||||
y: row,
|
||||
width: size.width,
|
||||
height: max_height,
|
||||
}
|
||||
}
|
||||
ViewportVariant::Fixed(area) => area,
|
||||
};
|
||||
self.set_viewport_area(next_area);
|
||||
self.clear()?;
|
||||
|
||||
self.last_known_size = size;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Queries the backend for size and resizes if it doesn't match the previous size.
|
||||
pub fn autoresize(&mut self) -> io::Result<()> {
|
||||
if self.viewport.resize_behavior == ResizeBehavior::Auto {
|
||||
let size = self.size()?;
|
||||
if size != self.viewport.area {
|
||||
self.resize(size)?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
fn set_viewport_area(&mut self, area: Rect) {
|
||||
self.viewport.area = area;
|
||||
self.buffers[self.current].resize(area);
|
||||
self.buffers[1 - self.current].resize(area);
|
||||
event!(Level::DEBUG, area = ?area, "viewport changed");
|
||||
}
|
||||
|
||||
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
|
||||
@@ -259,9 +304,12 @@ where
|
||||
where
|
||||
F: FnOnce(&mut Frame<B>),
|
||||
{
|
||||
let span = span!(Level::DEBUG, "draw");
|
||||
let _guard = span.enter();
|
||||
|
||||
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
|
||||
// and the terminal (if growing), which may OOB.
|
||||
self.autoresize()?;
|
||||
self.resize()?;
|
||||
|
||||
let mut frame = self.get_frame();
|
||||
f(&mut frame);
|
||||
@@ -287,9 +335,12 @@ where
|
||||
|
||||
// Flush
|
||||
self.backend.flush()?;
|
||||
|
||||
event!(Level::DEBUG, "completed frame");
|
||||
|
||||
Ok(CompletedFrame {
|
||||
buffer: &self.buffers[1 - self.current],
|
||||
area: self.viewport.area,
|
||||
area: self.last_known_size,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -310,12 +361,28 @@ where
|
||||
}
|
||||
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.backend.set_cursor(x, y)
|
||||
self.backend.set_cursor(x, y)?;
|
||||
self.last_known_cursor_pos = (x, y);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear the terminal and force a full redraw on the next draw call.
|
||||
pub fn clear(&mut self) -> io::Result<()> {
|
||||
self.backend.clear()?;
|
||||
event!(Level::DEBUG, "clear");
|
||||
match self.viewport.variant {
|
||||
ViewportVariant::Fullscreen => self.backend.clear(ClearType::All)?,
|
||||
ViewportVariant::Inline(_) => {
|
||||
self.backend
|
||||
.set_cursor(self.viewport.area.left(), self.viewport.area.top())?;
|
||||
self.backend.clear(ClearType::AfterCursor)?;
|
||||
}
|
||||
ViewportVariant::Fixed(area) => {
|
||||
for row in area.top()..area.bottom() {
|
||||
self.backend.set_cursor(0, row)?;
|
||||
self.backend.clear(ClearType::AfterCursor)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reset the back buffer to make sure the next update will redraw everything.
|
||||
self.buffers[1 - self.current].reset();
|
||||
Ok(())
|
||||
@@ -325,4 +392,97 @@ where
|
||||
pub fn size(&self) -> io::Result<Rect> {
|
||||
self.backend.size()
|
||||
}
|
||||
|
||||
/// Insert some content before the current inline viewport. This has no effect when the
|
||||
/// viewport is fullscreen.
|
||||
///
|
||||
/// This function scrolls down the current viewport by the given height. The newly freed space is
|
||||
/// then made available to the `draw_fn` closure through a writable `Buffer`.
|
||||
///
|
||||
/// Before:
|
||||
/// ```ignore
|
||||
/// +-------------------+
|
||||
/// | |
|
||||
/// | viewport |
|
||||
/// | |
|
||||
/// +-------------------+
|
||||
/// ```
|
||||
///
|
||||
/// After:
|
||||
/// ```ignore
|
||||
/// +-------------------+
|
||||
/// | buffer |
|
||||
/// +-------------------+
|
||||
/// +-------------------+
|
||||
/// | |
|
||||
/// | viewport |
|
||||
/// | |
|
||||
/// +-------------------+
|
||||
/// ```
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ## Insert a single line before the current viewport
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tui::widgets::{Paragraph, Widget};
|
||||
/// # use tui::text::{Spans, Span};
|
||||
/// # use tui::style::{Color, Style};
|
||||
/// # use tui::{Terminal};
|
||||
/// # use tui::backend::TestBackend;
|
||||
/// # let backend = TestBackend::new(10, 10);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// terminal.insert_before(1, |buf| {
|
||||
/// Paragraph::new(Spans::from(vec![
|
||||
/// Span::raw("This line will be added "),
|
||||
/// Span::styled("before", Style::default().fg(Color::Blue)),
|
||||
/// Span::raw(" the current viewport")
|
||||
/// ])).render(buf.area, buf);
|
||||
/// });
|
||||
/// ```
|
||||
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> io::Result<()>
|
||||
where
|
||||
F: FnOnce(&mut Buffer),
|
||||
{
|
||||
let span = span!(Level::DEBUG, "insert_before");
|
||||
let _guard = span.enter();
|
||||
if !matches!(self.viewport.variant, ViewportVariant::Inline(_)) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.clear()?;
|
||||
let height = height.min(self.last_known_size.height);
|
||||
self.backend.append_lines(height)?;
|
||||
let missing_lines =
|
||||
height.saturating_sub(self.last_known_size.bottom() - self.viewport.area.top());
|
||||
let area = Rect {
|
||||
x: self.viewport.area.left(),
|
||||
y: self.viewport.area.top().saturating_sub(missing_lines),
|
||||
width: self.viewport.area.width,
|
||||
height,
|
||||
};
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
draw_fn(&mut buffer);
|
||||
|
||||
let iter = buffer.content.iter().enumerate().map(|(i, c)| {
|
||||
let (x, y) = buffer.pos_of(i);
|
||||
(x, y, c)
|
||||
});
|
||||
self.backend.draw(iter)?;
|
||||
self.backend.flush()?;
|
||||
|
||||
let remaining_lines = self.last_known_size.height - area.bottom();
|
||||
let missing_lines = self.viewport.area.height.saturating_sub(remaining_lines);
|
||||
self.backend.append_lines(self.viewport.area.height)?;
|
||||
|
||||
self.set_viewport_area(Rect {
|
||||
x: area.left(),
|
||||
y: area.bottom().saturating_sub(missing_lines),
|
||||
width: area.width,
|
||||
height: self.viewport.area.height,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
16
src/text.rs
16
src/text.rs
@@ -194,15 +194,9 @@ impl<'a> From<&'a str> for Span<'a> {
|
||||
}
|
||||
|
||||
/// A string composed of clusters of graphemes, each with their own style.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct Spans<'a>(pub Vec<Span<'a>>);
|
||||
|
||||
impl<'a> Default for Spans<'a> {
|
||||
fn default() -> Spans<'a> {
|
||||
Spans(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Spans<'a> {
|
||||
/// Returns the width of the underlying string.
|
||||
///
|
||||
@@ -279,17 +273,11 @@ impl<'a> From<Spans<'a>> for String {
|
||||
/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
|
||||
/// assert_eq!(6, text.height());
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct Text<'a> {
|
||||
pub lines: Vec<Spans<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Default for Text<'a> {
|
||||
fn default() -> Text<'a> {
|
||||
Text { lines: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Text<'a> {
|
||||
/// Create some text (potentially multiple lines) with no style.
|
||||
///
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::{
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
text::Spans,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
use std::fmt::Debug;
|
||||
@@ -26,10 +27,9 @@ pub trait Shape {
|
||||
/// Label to draw some text on the canvas
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Label<'a> {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub text: &'a str,
|
||||
pub color: Color,
|
||||
x: f64,
|
||||
y: f64,
|
||||
spans: Spans<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -293,8 +293,15 @@ impl<'a> Context<'a> {
|
||||
}
|
||||
|
||||
/// Print a string on the canvas at the given position
|
||||
pub fn print(&mut self, x: f64, y: f64, text: &'a str, color: Color) {
|
||||
self.labels.push(Label { x, y, text, color });
|
||||
pub fn print<T>(&mut self, x: f64, y: f64, spans: T)
|
||||
where
|
||||
T: Into<Spans<'a>>,
|
||||
{
|
||||
self.labels.push(Label {
|
||||
x,
|
||||
y,
|
||||
spans: spans.into(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Push the last layer if necessary
|
||||
@@ -433,6 +440,8 @@ where
|
||||
None => area,
|
||||
};
|
||||
|
||||
buf.set_style(canvas_area, Style::default().bg(self.background_color));
|
||||
|
||||
let width = canvas_area.width as usize;
|
||||
|
||||
let painter = match self.painter {
|
||||
@@ -464,14 +473,12 @@ where
|
||||
let (x, y) = (i % width, i / width);
|
||||
buf.get_mut(x as u16 + canvas_area.left(), y as u16 + canvas_area.top())
|
||||
.set_char(ch)
|
||||
.set_fg(color)
|
||||
.set_bg(self.background_color);
|
||||
.set_fg(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally draw the labels
|
||||
let style = Style::default().bg(self.background_color);
|
||||
let left = self.x_bounds[0];
|
||||
let right = self.x_bounds[1];
|
||||
let top = self.y_bounds[1];
|
||||
@@ -490,13 +497,7 @@ where
|
||||
{
|
||||
let x = ((label.x - left) * resolution.0 / width) as u16 + canvas_area.left();
|
||||
let y = ((top - label.y) * resolution.1 / height) as u16 + canvas_area.top();
|
||||
buf.set_stringn(
|
||||
x,
|
||||
y,
|
||||
label.text,
|
||||
(canvas_area.right() - x) as usize,
|
||||
style.fg(label.color),
|
||||
);
|
||||
buf.set_spans(x, y, &label.spans, canvas_area.right() - x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6122,6 +6122,7 @@ pub static WORLD_LOW_RESOLUTION: [(f64, f64); 1166] = [
|
||||
(120.43, 16.43),
|
||||
(121.72, 18.40),
|
||||
(125.34, 9.79),
|
||||
#[allow(clippy::approx_constant)]
|
||||
(125.56, 6.28),
|
||||
(122.38, 7.00),
|
||||
(125.10, 9.38),
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
use std::{borrow::Cow, cmp::max};
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::layout::Alignment;
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Rect},
|
||||
@@ -9,8 +14,6 @@ use crate::{
|
||||
Block, Borders, Widget,
|
||||
},
|
||||
};
|
||||
use std::{borrow::Cow, cmp::max};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// An X or Y axis for the chart widget
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -23,6 +26,8 @@ pub struct Axis<'a> {
|
||||
labels: Option<Vec<Span<'a>>>,
|
||||
/// The style used to draw the axis itself
|
||||
style: Style,
|
||||
/// The alignment of the labels of the Axis
|
||||
labels_alignment: Alignment,
|
||||
}
|
||||
|
||||
impl<'a> Default for Axis<'a> {
|
||||
@@ -32,6 +37,7 @@ impl<'a> Default for Axis<'a> {
|
||||
bounds: [0.0, 0.0],
|
||||
labels: None,
|
||||
style: Default::default(),
|
||||
labels_alignment: Alignment::Left,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,6 +77,15 @@ impl<'a> Axis<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Defines the alignment of the labels of the axis.
|
||||
/// The alignment behaves differently based on the axis:
|
||||
/// - Y-Axis: The labels are aligned within the area on the left of the axis
|
||||
/// - X-Axis: The first X-axis label is aligned relative to the Y-axis
|
||||
pub fn labels_alignment(mut self, alignment: Alignment) -> Axis<'a> {
|
||||
self.labels_alignment = alignment;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to determine which style of graphing to use
|
||||
@@ -141,7 +156,7 @@ impl<'a> Dataset<'a> {
|
||||
|
||||
/// A container that holds all the infos about where to display each elements of the chart (axis,
|
||||
/// labels, legend, ...).
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
struct ChartLayout {
|
||||
/// Location of the title of the x axis
|
||||
title_x: Option<(u16, u16)>,
|
||||
@@ -161,21 +176,6 @@ struct ChartLayout {
|
||||
graph_area: Rect,
|
||||
}
|
||||
|
||||
impl Default for ChartLayout {
|
||||
fn default() -> ChartLayout {
|
||||
ChartLayout {
|
||||
title_x: None,
|
||||
title_y: None,
|
||||
label_x: None,
|
||||
label_y: None,
|
||||
axis_x: None,
|
||||
axis_y: None,
|
||||
legend_area: None,
|
||||
graph_area: Rect::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget to plot one or more dataset in a cartesian coordinate system
|
||||
///
|
||||
/// # Examples
|
||||
@@ -297,7 +297,7 @@ impl<'a> Chart<'a> {
|
||||
}
|
||||
|
||||
layout.label_y = self.y_axis.labels.as_ref().and(Some(x));
|
||||
x += self.max_width_of_labels_left_of_y_axis(area);
|
||||
x += self.max_width_of_labels_left_of_y_axis(area, self.y_axis.labels.is_some());
|
||||
|
||||
if self.x_axis.labels.is_some() && y > area.top() {
|
||||
layout.axis_x = Some(y);
|
||||
@@ -353,17 +353,26 @@ impl<'a> Chart<'a> {
|
||||
layout
|
||||
}
|
||||
|
||||
fn max_width_of_labels_left_of_y_axis(&self, area: Rect) -> u16 {
|
||||
fn max_width_of_labels_left_of_y_axis(&self, area: Rect, has_y_axis: bool) -> u16 {
|
||||
let mut max_width = self
|
||||
.y_axis
|
||||
.labels
|
||||
.as_ref()
|
||||
.map(|l| l.iter().map(Span::width).max().unwrap_or_default() as u16)
|
||||
.unwrap_or_default();
|
||||
if let Some(ref x_labels) = self.x_axis.labels {
|
||||
if !x_labels.is_empty() {
|
||||
max_width = max(max_width, x_labels[0].content.width() as u16);
|
||||
}
|
||||
|
||||
if let Some(first_x_label) = self.x_axis.labels.as_ref().and_then(|labels| labels.get(0)) {
|
||||
let first_label_width = first_x_label.content.width() as u16;
|
||||
let width_left_of_y_axis = match self.x_axis.labels_alignment {
|
||||
Alignment::Left => {
|
||||
// The last character of the label should be below the Y-Axis when it exists, not on its left
|
||||
let y_axis_offset = if has_y_axis { 1 } else { 0 };
|
||||
first_label_width.saturating_sub(y_axis_offset)
|
||||
}
|
||||
Alignment::Center => first_label_width / 2,
|
||||
Alignment::Right => 0,
|
||||
};
|
||||
max_width = max(max_width, width_left_of_y_axis);
|
||||
}
|
||||
// labels of y axis and first label of x axis can take at most 1/3rd of the total width
|
||||
max_width.min(area.width / 3)
|
||||
@@ -385,26 +394,73 @@ impl<'a> Chart<'a> {
|
||||
if labels_len < 2 {
|
||||
return;
|
||||
}
|
||||
let width_between_ticks = graph_area.width / (labels_len - 1);
|
||||
for (i, label) in labels.iter().enumerate() {
|
||||
let label_width = label.width() as u16;
|
||||
let label_width = if i == 0 {
|
||||
// the first label is put between the left border of the chart and the y axis.
|
||||
graph_area
|
||||
.left()
|
||||
.saturating_sub(chart_area.left())
|
||||
.min(label_width)
|
||||
} else {
|
||||
// other labels are put on the left of each tick on the x axis
|
||||
width_between_ticks.min(label_width)
|
||||
};
|
||||
buf.set_span(
|
||||
graph_area.left() + i as u16 * width_between_ticks - label_width,
|
||||
y,
|
||||
label,
|
||||
label_width,
|
||||
);
|
||||
|
||||
let width_between_ticks = graph_area.width / labels_len;
|
||||
|
||||
let label_area = self.first_x_label_area(
|
||||
y,
|
||||
labels.first().unwrap().width() as u16,
|
||||
width_between_ticks,
|
||||
chart_area,
|
||||
graph_area,
|
||||
);
|
||||
|
||||
let label_alignment = match self.x_axis.labels_alignment {
|
||||
Alignment::Left => Alignment::Right,
|
||||
Alignment::Center => Alignment::Center,
|
||||
Alignment::Right => Alignment::Left,
|
||||
};
|
||||
|
||||
Self::render_label(buf, labels.first().unwrap(), label_area, label_alignment);
|
||||
|
||||
for (i, label) in labels[1..labels.len() - 1].iter().enumerate() {
|
||||
// We add 1 to x (and width-1 below) to leave at least one space before each intermediate labels
|
||||
let x = graph_area.left() + (i + 1) as u16 * width_between_ticks + 1;
|
||||
let label_area = Rect::new(x, y, width_between_ticks.saturating_sub(1), 1);
|
||||
|
||||
Self::render_label(buf, label, label_area, Alignment::Center);
|
||||
}
|
||||
|
||||
let x = graph_area.right() - width_between_ticks;
|
||||
let label_area = Rect::new(x, y, width_between_ticks, 1);
|
||||
// The last label should be aligned Right to be at the edge of the graph area
|
||||
Self::render_label(buf, labels.last().unwrap(), label_area, Alignment::Right);
|
||||
}
|
||||
|
||||
fn first_x_label_area(
|
||||
&self,
|
||||
y: u16,
|
||||
label_width: u16,
|
||||
max_width_after_y_axis: u16,
|
||||
chart_area: Rect,
|
||||
graph_area: Rect,
|
||||
) -> Rect {
|
||||
let (min_x, max_x) = match self.x_axis.labels_alignment {
|
||||
Alignment::Left => (chart_area.left(), graph_area.left()),
|
||||
Alignment::Center => (
|
||||
chart_area.left(),
|
||||
graph_area.left() + max_width_after_y_axis.min(label_width),
|
||||
),
|
||||
Alignment::Right => (
|
||||
graph_area.left().saturating_sub(1),
|
||||
graph_area.left() + max_width_after_y_axis,
|
||||
),
|
||||
};
|
||||
|
||||
Rect::new(min_x, y, max_x - min_x, 1)
|
||||
}
|
||||
|
||||
fn render_label(buf: &mut Buffer, label: &Span, label_area: Rect, alignment: Alignment) {
|
||||
let label_width = label.width() as u16;
|
||||
let bounded_label_width = label_area.width.min(label_width);
|
||||
|
||||
let x = match alignment {
|
||||
Alignment::Left => label_area.left(),
|
||||
Alignment::Center => label_area.left() + label_area.width / 2 - bounded_label_width / 2,
|
||||
Alignment::Right => label_area.right() - bounded_label_width,
|
||||
};
|
||||
|
||||
buf.set_span(x, label_area.top(), label, bounded_label_width);
|
||||
}
|
||||
|
||||
fn render_y_labels(
|
||||
@@ -420,11 +476,16 @@ impl<'a> Chart<'a> {
|
||||
};
|
||||
let labels = self.y_axis.labels.as_ref().unwrap();
|
||||
let labels_len = labels.len() as u16;
|
||||
let label_width = graph_area.left().saturating_sub(chart_area.left());
|
||||
for (i, label) in labels.iter().enumerate() {
|
||||
let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1);
|
||||
if dy < graph_area.bottom() {
|
||||
buf.set_span(x, graph_area.bottom() - 1 - dy, label, label_width as u16);
|
||||
let label_area = Rect::new(
|
||||
x,
|
||||
graph_area.bottom().saturating_sub(1) - dy,
|
||||
(graph_area.left() - chart_area.left()).saturating_sub(1),
|
||||
1,
|
||||
);
|
||||
Self::render_label(buf, label, label_area, self.y_axis.labels_alignment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,21 +7,12 @@ use crate::{
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ListState {
|
||||
offset: usize,
|
||||
selected: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for ListState {
|
||||
fn default() -> ListState {
|
||||
ListState {
|
||||
offset: 0,
|
||||
selected: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ListState {
|
||||
pub fn selected(&self) -> Option<usize> {
|
||||
self.selected
|
||||
@@ -87,6 +78,8 @@ pub struct List<'a> {
|
||||
highlight_style: Style,
|
||||
/// Symbol in front of the selected item (Shift all items to the right)
|
||||
highlight_symbol: Option<&'a str>,
|
||||
/// Whether to repeat the highlight symbol for each line of the selected item
|
||||
repeat_highlight_symbol: bool,
|
||||
}
|
||||
|
||||
impl<'a> List<'a> {
|
||||
@@ -101,6 +94,7 @@ impl<'a> List<'a> {
|
||||
start_corner: Corner::TopLeft,
|
||||
highlight_style: Style::default(),
|
||||
highlight_symbol: None,
|
||||
repeat_highlight_symbol: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +118,11 @@ impl<'a> List<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn repeat_highlight_symbol(mut self, repeat: bool) -> List<'a> {
|
||||
self.repeat_highlight_symbol = repeat;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn start_corner(mut self, corner: Corner) -> List<'a> {
|
||||
self.start_corner = corner;
|
||||
self
|
||||
@@ -227,19 +226,27 @@ impl<'a> StatefulWidget for List<'a> {
|
||||
buf.set_style(area, item_style);
|
||||
|
||||
let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
|
||||
let elem_x = if has_selection {
|
||||
let symbol = if is_selected {
|
||||
for (j, line) in item.content.lines.iter().enumerate() {
|
||||
// if the item is selected, we need to display the hightlight symbol:
|
||||
// - either for the first line of the item only,
|
||||
// - or for each line of the item if the appropriate option is set
|
||||
let symbol = if is_selected && (j == 0 || self.repeat_highlight_symbol) {
|
||||
highlight_symbol
|
||||
} else {
|
||||
&blank_symbol
|
||||
};
|
||||
let (x, _) = buf.set_stringn(x, y, symbol, list_area.width as usize, item_style);
|
||||
x
|
||||
} else {
|
||||
x
|
||||
};
|
||||
let max_element_width = (list_area.width - (elem_x - x)) as usize;
|
||||
for (j, line) in item.content.lines.iter().enumerate() {
|
||||
let (elem_x, max_element_width) = if has_selection {
|
||||
let (elem_x, _) = buf.set_stringn(
|
||||
x,
|
||||
y + j as u16,
|
||||
symbol,
|
||||
list_area.width as usize,
|
||||
item_style,
|
||||
);
|
||||
(elem_x, (list_area.width - (elem_x - x)) as u16)
|
||||
} else {
|
||||
(x, list_area.width)
|
||||
};
|
||||
buf.set_spans(elem_x, y + j as u16, line, max_element_width as u16);
|
||||
}
|
||||
if is_selected {
|
||||
|
||||
@@ -87,7 +87,7 @@ pub trait Widget {
|
||||
/// ```rust,no_run
|
||||
/// # use std::io;
|
||||
/// # use tui::Terminal;
|
||||
/// # use tui::backend::{Backend, TermionBackend};
|
||||
/// # use tui::backend::{Backend, TestBackend};
|
||||
/// # use tui::widgets::{Widget, List, ListItem, ListState};
|
||||
///
|
||||
/// // Let's say we have some events to display.
|
||||
@@ -154,9 +154,8 @@ pub trait Widget {
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let stdout = io::stdout();
|
||||
/// let backend = TermionBackend::new(stdout);
|
||||
/// let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
///
|
||||
/// let mut events = Events::new(vec![
|
||||
/// String::from("Item 1"),
|
||||
|
||||
@@ -403,8 +403,8 @@ mod test {
|
||||
let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\
|
||||
では、";
|
||||
let (word_wrapper, word_wrapper_width) =
|
||||
run_composer(Composer::WordWrapper { trim: true }, &text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width);
|
||||
run_composer(Composer::WordWrapper { trim: true }, text, width);
|
||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
||||
assert_eq!(line_truncator, vec!["コンピュータ上で文字"]);
|
||||
let wrapped = vec![
|
||||
"コンピュータ上で文字",
|
||||
|
||||
@@ -335,21 +335,12 @@ impl<'a> Table<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TableState {
|
||||
offset: usize,
|
||||
selected: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for TableState {
|
||||
fn default() -> TableState {
|
||||
TableState {
|
||||
offset: 0,
|
||||
selected: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TableState {
|
||||
pub fn selected(&self) -> Option<usize> {
|
||||
self.selected
|
||||
|
||||
@@ -8,14 +8,14 @@ fn backend_termion_should_only_write_diffs() -> Result<(), Box<dyn std::error::E
|
||||
{
|
||||
use tui::{
|
||||
backend::TermionBackend, layout::Rect, widgets::Paragraph, Terminal, TerminalOptions,
|
||||
Viewport,
|
||||
ViewportVariant,
|
||||
};
|
||||
let backend = TermionBackend::new(&mut stdout);
|
||||
let area = Rect::new(0, 0, 3, 1);
|
||||
let mut terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::fixed(area),
|
||||
viewport: ViewportVariant::Fixed(area),
|
||||
},
|
||||
)?;
|
||||
terminal.draw(|f| {
|
||||
|
||||
42
tests/widgets_canvas.rs
Normal file
42
tests/widgets_canvas.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use tui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
style::{Color, Style},
|
||||
text::Span,
|
||||
widgets::canvas::Canvas,
|
||||
Terminal,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn widgets_canvas_draw_labels() {
|
||||
let backend = TestBackend::new(5, 5);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let label = String::from("test");
|
||||
let canvas = Canvas::default()
|
||||
.background_color(Color::Yellow)
|
||||
.x_bounds([0.0, 5.0])
|
||||
.y_bounds([0.0, 5.0])
|
||||
.paint(|ctx| {
|
||||
ctx.print(
|
||||
0.0,
|
||||
0.0,
|
||||
Span::styled(label.clone(), Style::default().fg(Color::Blue)),
|
||||
);
|
||||
});
|
||||
f.render_widget(canvas, f.size());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut expected = Buffer::with_lines(vec![" ", " ", " ", " ", "test "]);
|
||||
for row in 0..5 {
|
||||
for col in 0..5 {
|
||||
expected.get_mut(col, row).set_bg(Color::Yellow);
|
||||
}
|
||||
}
|
||||
for col in 0..4 {
|
||||
expected.get_mut(col, 4).set_fg(Color::Blue);
|
||||
}
|
||||
terminal.backend().assert_buffer(&expected)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use tui::layout::Alignment;
|
||||
use tui::{
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
@@ -13,6 +14,22 @@ fn create_labels<'a>(labels: &'a [&'a str]) -> Vec<Span<'a>> {
|
||||
labels.iter().map(|l| Span::from(*l)).collect()
|
||||
}
|
||||
|
||||
fn axis_test_case<S>(width: u16, height: u16, x_axis: Axis, y_axis: Axis, lines: Vec<S>)
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let backend = TestBackend::new(width, height);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let chart = Chart::new(vec![]).x_axis(x_axis).y_axis(y_axis);
|
||||
f.render_widget(chart, f.size());
|
||||
})
|
||||
.unwrap();
|
||||
let expected = Buffer::with_lines(lines);
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_chart_can_render_on_small_areas() {
|
||||
let test_case = |width, height| {
|
||||
@@ -49,33 +66,26 @@ fn widgets_chart_can_render_on_small_areas() {
|
||||
|
||||
#[test]
|
||||
fn widgets_chart_handles_long_labels() {
|
||||
let test_case = |x_labels, y_labels, lines| {
|
||||
let backend = TestBackend::new(10, 5);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let datasets = vec![Dataset::default()
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Magenta))
|
||||
.data(&[(2.0, 2.0)])];
|
||||
let mut x_axis = Axis::default().bounds([0.0, 1.0]);
|
||||
if let Some((left_label, right_label)) = x_labels {
|
||||
x_axis = x_axis.labels(vec![Span::from(left_label), Span::from(right_label)]);
|
||||
}
|
||||
let mut y_axis = Axis::default().bounds([0.0, 1.0]);
|
||||
if let Some((left_label, right_label)) = y_labels {
|
||||
y_axis = y_axis.labels(vec![Span::from(left_label), Span::from(right_label)]);
|
||||
}
|
||||
let chart = Chart::new(datasets).x_axis(x_axis).y_axis(y_axis);
|
||||
f.render_widget(chart, f.size());
|
||||
})
|
||||
.unwrap();
|
||||
let expected = Buffer::with_lines(lines);
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
let test_case = |x_labels, y_labels, x_alignment, lines| {
|
||||
let mut x_axis = Axis::default().bounds([0.0, 1.0]);
|
||||
if let Some((left_label, right_label)) = x_labels {
|
||||
x_axis = x_axis
|
||||
.labels(vec![Span::from(left_label), Span::from(right_label)])
|
||||
.labels_alignment(x_alignment);
|
||||
}
|
||||
|
||||
let mut y_axis = Axis::default().bounds([0.0, 1.0]);
|
||||
if let Some((left_label, right_label)) = y_labels {
|
||||
y_axis = y_axis.labels(vec![Span::from(left_label), Span::from(right_label)]);
|
||||
}
|
||||
|
||||
axis_test_case(10, 5, x_axis, y_axis, lines);
|
||||
};
|
||||
|
||||
test_case(
|
||||
Some(("AAAA", "B")),
|
||||
None,
|
||||
Alignment::Left,
|
||||
vec![
|
||||
" ",
|
||||
" ",
|
||||
@@ -87,6 +97,7 @@ fn widgets_chart_handles_long_labels() {
|
||||
test_case(
|
||||
Some(("A", "BBBB")),
|
||||
None,
|
||||
Alignment::Left,
|
||||
vec![
|
||||
" ",
|
||||
" ",
|
||||
@@ -98,6 +109,7 @@ fn widgets_chart_handles_long_labels() {
|
||||
test_case(
|
||||
Some(("AAAAAAAAAAA", "B")),
|
||||
None,
|
||||
Alignment::Left,
|
||||
vec![
|
||||
" ",
|
||||
" ",
|
||||
@@ -109,6 +121,7 @@ fn widgets_chart_handles_long_labels() {
|
||||
test_case(
|
||||
Some(("A", "B")),
|
||||
Some(("CCCCCCC", "D")),
|
||||
Alignment::Left,
|
||||
vec![
|
||||
"D │ ",
|
||||
" │ ",
|
||||
@@ -117,6 +130,129 @@ fn widgets_chart_handles_long_labels() {
|
||||
" A B",
|
||||
],
|
||||
);
|
||||
test_case(
|
||||
Some(("AAAAAAAAAA", "B")),
|
||||
Some(("C", "D")),
|
||||
Alignment::Center,
|
||||
vec![
|
||||
"D │ ",
|
||||
" │ ",
|
||||
"C │ ",
|
||||
" └──────",
|
||||
"AAAAAAA B",
|
||||
],
|
||||
);
|
||||
test_case(
|
||||
Some(("AAAAAAA", "B")),
|
||||
Some(("C", "D")),
|
||||
Alignment::Right,
|
||||
vec![
|
||||
"D│ ",
|
||||
" │ ",
|
||||
"C│ ",
|
||||
" └────────",
|
||||
" AAAAA B",
|
||||
],
|
||||
);
|
||||
test_case(
|
||||
Some(("AAAAAAA", "BBBBBBB")),
|
||||
Some(("C", "D")),
|
||||
Alignment::Right,
|
||||
vec![
|
||||
"D│ ",
|
||||
" │ ",
|
||||
"C│ ",
|
||||
" └────────",
|
||||
" AAAAABBBB",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_chart_handles_x_axis_labels_alignments() {
|
||||
let test_case = |y_alignment, lines| {
|
||||
let x_axis = Axis::default()
|
||||
.labels(vec![Span::from("AAAA"), Span::from("B"), Span::from("C")])
|
||||
.labels_alignment(y_alignment);
|
||||
|
||||
let y_axis = Axis::default();
|
||||
|
||||
axis_test_case(10, 5, x_axis, y_axis, lines);
|
||||
};
|
||||
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ───────",
|
||||
"AAA B C",
|
||||
],
|
||||
);
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ────────",
|
||||
"AAAA B C",
|
||||
],
|
||||
);
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
vec![
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
"──────────",
|
||||
"AAA B C",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_chart_handles_y_axis_labels_alignments() {
|
||||
let test_case = |y_alignment, lines| {
|
||||
let x_axis = Axis::default().labels(create_labels(&["AAAAA", "B"]));
|
||||
|
||||
let y_axis = Axis::default()
|
||||
.labels(create_labels(&["C", "D"]))
|
||||
.labels_alignment(y_alignment);
|
||||
|
||||
axis_test_case(20, 5, x_axis, y_axis, lines);
|
||||
};
|
||||
test_case(
|
||||
Alignment::Left,
|
||||
vec![
|
||||
"D │ ",
|
||||
" │ ",
|
||||
"C │ ",
|
||||
" └───────────────",
|
||||
"AAAAA B",
|
||||
],
|
||||
);
|
||||
test_case(
|
||||
Alignment::Center,
|
||||
vec![
|
||||
" D │ ",
|
||||
" │ ",
|
||||
" C │ ",
|
||||
" └───────────────",
|
||||
"AAAAA B",
|
||||
],
|
||||
);
|
||||
test_case(
|
||||
Alignment::Right,
|
||||
vec![
|
||||
" D│ ",
|
||||
" │ ",
|
||||
" C│ ",
|
||||
" └───────────────",
|
||||
"AAAAA B",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -329,7 +465,7 @@ fn widgets_chart_can_have_a_legend() {
|
||||
"│ │ •• •• │",
|
||||
"│0.0 │• X Axis│",
|
||||
"│ └─────────────────────────────────────────────────────│",
|
||||
"│ 0.0 50.0 100.0 │",
|
||||
"│ 0.0 50.0 100.0│",
|
||||
"└──────────────────────────────────────────────────────────┘",
|
||||
]);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use tui::{
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
text::Spans,
|
||||
widgets::{Block, Borders, List, ListItem, ListState},
|
||||
Terminal,
|
||||
};
|
||||
@@ -126,3 +127,74 @@ fn widgets_list_should_clamp_offset_if_items_are_removed() {
|
||||
let expected = Buffer::with_lines(vec![" Item 3 ", " ", " ", " "]);
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_list_should_display_multiline_items() {
|
||||
let backend = TestBackend::new(10, 6);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
let mut state = ListState::default();
|
||||
state.select(Some(1));
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let items = vec![
|
||||
ListItem::new(vec![Spans::from("Item 1"), Spans::from("Item 1a")]),
|
||||
ListItem::new(vec![Spans::from("Item 2"), Spans::from("Item 2b")]),
|
||||
ListItem::new(vec![Spans::from("Item 3"), Spans::from("Item 3c")]),
|
||||
];
|
||||
let list = List::new(items)
|
||||
.highlight_style(Style::default().bg(Color::Yellow))
|
||||
.highlight_symbol(">> ");
|
||||
f.render_stateful_widget(list, size, &mut state);
|
||||
})
|
||||
.unwrap();
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
" Item 1 ",
|
||||
" Item 1a",
|
||||
">> Item 2 ",
|
||||
" Item 2b",
|
||||
" Item 3 ",
|
||||
" Item 3c",
|
||||
]);
|
||||
for x in 0..10 {
|
||||
expected.get_mut(x, 2).set_bg(Color::Yellow);
|
||||
expected.get_mut(x, 3).set_bg(Color::Yellow);
|
||||
}
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn widgets_list_should_repeat_highlight_symbol() {
|
||||
let backend = TestBackend::new(10, 6);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
let mut state = ListState::default();
|
||||
state.select(Some(1));
|
||||
terminal
|
||||
.draw(|f| {
|
||||
let size = f.size();
|
||||
let items = vec![
|
||||
ListItem::new(vec![Spans::from("Item 1"), Spans::from("Item 1a")]),
|
||||
ListItem::new(vec![Spans::from("Item 2"), Spans::from("Item 2b")]),
|
||||
ListItem::new(vec![Spans::from("Item 3"), Spans::from("Item 3c")]),
|
||||
];
|
||||
let list = List::new(items)
|
||||
.highlight_style(Style::default().bg(Color::Yellow))
|
||||
.highlight_symbol(">> ")
|
||||
.repeat_highlight_symbol(true);
|
||||
f.render_stateful_widget(list, size, &mut state);
|
||||
})
|
||||
.unwrap();
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
" Item 1 ",
|
||||
" Item 1a",
|
||||
">> Item 2 ",
|
||||
">> Item 2b",
|
||||
" Item 3 ",
|
||||
" Item 3c",
|
||||
]);
|
||||
for x in 0..10 {
|
||||
expected.get_mut(x, 2).set_bg(Color::Yellow);
|
||||
expected.get_mut(x, 3).set_bg(Color::Yellow);
|
||||
}
|
||||
terminal.backend().assert_buffer(&expected);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user