Compare commits

..

52 Commits

Author SHA1 Message Date
Dylan Knutson
7f68927467 Add TextInput widget, Interaction* types for interactive widgets 2022-07-08 11:10:36 -07:00
♫ Christian Krause ♫
a6b25a4877 chore: add panic hook example (#593)
Without a terminal-resetting panic hook there are two main problems when
an application panics:

1.  The report of the panic is distorted because the terminal has not
    properly left the alternate screen and is still in raw mode.

2.  The terminal needs to be manually reset with the `reset` command.

To avoid this, the standard panic hook can be extended to first reset
the terminal.
2022-04-24 16:49:57 +02:00
Florian Dehau
90d8cb6526 chore: add more apps using tui to the README 2022-04-24 15:49:03 +02:00
Florian Dehau
e71faa988e Release v0.18.0 2022-04-24 15:03:09 +02:00
Atk
ed0ae81aae chore: update crossterm to v0.23 (#598) 2022-04-24 14:47:54 +02:00
David
a61b078dea chore: fix clippy warning (#601) 2022-04-24 14:25:50 +02:00
Florian Dehau
85939306e3 Release v0.17.0 2022-01-22 13:30:35 +01:00
Florian Dehau
cf2d9c2c1d feat!: bump MSRV to 1.56.1 and migrate to edition 2021 2022-01-22 13:18:56 +01:00
theogilbert
853d9047b0 feat(widgets/chart): add option to control alignment of axis labels (#568)
* feat(chart): allow custom alignment of first X-Axis label

* refactor(chart): rename ambiguous function parameter

* feat(chart): allow custom alignment of Y-Axis labels

* refactor(chart): refactor axis test cases

* refactor(chart): rename minor variable

* fix(chart): force centered x-axis label near Y-Axis

* fix(chart): fix subtract overflow on small rendering area

* refactor(chart): rename alignment property

* refactor(chart): merge two nested conditions

* refactor(chart): decompose x labels rendering loop
2021-12-23 19:02:10 +01:00
Florian Dehau
6069d89dee chore: fix all clippy warnings 2021-12-23 18:55:43 +01:00
Florian Dehau
d25e263b8e chore: enable clippy on all targets 2021-12-23 18:55:43 +01:00
ljedrz
d05e696d45 chore: fix optional attribute for serde feature (#571)
Signed-off-by: ljedrz <ljedrz@gmail.com>
2021-12-23 18:51:13 +01:00
Petr Portnov
ef583cead9 chore(examples): remove unused demo/util.rs
This module is unused and is not imported by any other module.
2021-11-30 22:35:45 +01:00
Denis
90c4da4e68 Update README.md 2021-11-30 22:25:00 +01:00
Florian Dehau
8032191366 chore: fix table example
Third column in table example was using the `Max` constraint.

But since version 0.16, the layout system does not add a hidden constraint on the last column which would ensure that it fills the remaining available space (a change that was already mentioned in #525). In addition, `tui` does not support sizing based on content because of its immediate mode nature. Therefore, `Max` is now resolved to `0`. Replacing with `Min` fixes the issue.

A new way of specifying constraints is being worked on at #519 which should for more deterministic and advanced layout.
2021-11-21 21:00:34 +01:00
Florian Dehau
c8c03294e1 chore: self contained examples 2021-11-11 16:18:49 +01:00
wcampbell
e00df22588 chore: add adsb_deku/radar to apps using tui (#555)
My `adsb_deku/radar` application uses tui, using the Table and Canvas to show
information and plot airplanes on a latitude/longitude coordinates map.

Signed-off-by: wcampbell <wcampbell1995@gmail.com>
2021-11-11 16:13:42 +01:00
Florian Dehau
9806217a6a feat!: use crossterm as default backend 2021-11-01 23:21:55 +01:00
Florian Dehau
1be5cf2d90 chore: add joshuto to the apps using tui 2021-10-17 19:39:15 +02:00
Florian Dehau
ca68bae4ed feat!(widgets/canvas): use spans for text of labels 2021-10-17 18:55:59 +02:00
Florian Dehau
8c1f58079f chore: fix build 2021-10-17 17:27:32 +02:00
Antoine Büsch
4845c03eec feat(widgets/list): repeat highlight symbol on multi-line items (#533)
When this option is true, the hightlight symbol is repeated for each
line of the selected item, instead of just the first line.
2021-10-17 17:05:51 +02:00
Florian Dehau
532a595c41 chore: pin bitflags version to 1.3 2021-10-17 16:20:40 +02:00
Antoine Büsch
25ce5bc90b chore: bump the minimum supported Rust version to 1.52.1 (#534)
- `const_fn` usage in the `bitflags` crate.
- `unsafe_op_in_unsafe_fn` lint usage in `rust_info` despite pinned `cargo-make` version.
2021-10-17 15:49:31 +02:00
JerzySpendel
80a929ccc6 chore: fix typo (#513) 2021-08-08 11:10:21 +02:00
Christian Visintin
3797863e14 chore: add termscp to list of apps using tui (#510) 2021-08-01 23:08:19 +02:00
Florian Dehau
7870793b4b Release v0.16.0 2021-08-01 20:12:20 +02:00
Florian Dehau
a7c21a9729 fix(widgets): avoid offset panic in Table and List when input changes 2021-08-01 18:01:24 +02:00
Florian Dehau
914d54e672 chore: bump crossterm to 0.20 2021-08-01 17:14:11 +02:00
Florian Dehau
a68e38e59e fix(table): use Layout in table column widths computation 2021-08-01 16:46:54 +02:00
Florian Dehau
e870e5d8a5 feat(layout): add private option to control last chunk expansion 2021-08-01 16:46:54 +02:00
Deepu K Sasidharan
29387e785c Add battleship.rs 2021-08-01 16:44:36 +02:00
Florian Dehau
8eb6336f5e refactor(widgets): remove iter::repeat for blank symbols 2021-08-01 15:08:53 +02:00
Florian Dehau
34a2be6458 fix(widgets/chart): remove panics with long axis labels 2021-08-01 15:08:53 +02:00
Florian Dehau
fbd834469f doc(widgets/clear): clarify usage of clear 2021-06-16 21:58:04 +02:00
Florian Dehau
8da5f740af refactor(examples): show more use case in gauge example 2021-06-16 20:24:14 +02:00
Florian Dehau
38dcddb126 fix(widgets/gauge): apply label style and avoid overflow on large labels 2021-06-16 20:24:14 +02:00
Phillip Cloud
92948d2394 chore: add minesweep to list of apps using tui-rs 2021-06-16 17:25:11 +02:00
orhun
a3a0a80a02 docs: add gpg-tui to the "apps using tui" list 2021-06-16 17:19:34 +02:00
jmrgibson
a5f7019b2a doc: fix minor grammatical errors (#489)
A missing "and" after "an" (which I do all the time) and some tense clarification.
2021-06-16 17:15:13 +02:00
Moritz
e05b80cec1 doc: fix typos in comments. (#486) 2021-06-16 17:14:16 +02:00
Florian Dehau
23d5fbde56 refactor(examples): remove exit key from Events handler
The thread spawned by `Events` to listen for keyboard inputs had knowlegde of
the exit key to exit on its own when it was pressed. It is however a source of
confusion (#491) because the exit behavior is wired in both the event handler
and the input handling performed by the app. In addition, this is not needed as
the thread will exit anyway when the main thread finishes as it is already the
case for the "tick" thread. Therefore, this commit removes both the option to
configure the exit key in the `Events` handler and the option to temporarily
ignore it.
2021-06-16 17:07:59 +02:00
Oleks (オレクス)
a346704cdc feat(block): add option to center and right align the title (#462)
* Added ability to set title alignment, added tests, modified blocks example to show the feature

* Added test for inner with title in block

* Updated block example to show center alignment

* Formatting fixed

* Updated tests to use lamdas and be more concise. Updated title alignmnet code to be more straightforward and have correct behavior when placing title in the center without left border
2021-05-22 16:55:24 +02:00
Andrew Chin
24396d97ed doc: Add doctests that shows how Text can be created from Cow<str> 2021-05-22 16:47:06 +02:00
Andrew Chin
703e41cd49 feat(Text): Add a From<Cow<str>> impl for Text 2021-05-22 16:47:06 +02:00
Florian Dehau
975c4165d0 chore: fix clippy warnings 2021-05-22 15:54:38 +02:00
Arijit Basu
dbf38d847a Add xplr to the "apps using tui" list
`xplr` is a hackable TUI file explorer.
2021-05-22 15:08:06 +02:00
Florian Dehau
91a2519cc3 chore: update links to examples in README
Links now include the fully qualified domain as well as the version.
This will make them work in docs.rs and make sure readers are looking at code which is consistent with the latest version available.
2021-05-03 00:30:59 +02:00
Alexandru Scvortov
a1c3ba2088 fix: actually clear buffer in TestBackend::clear (#461) 2021-05-02 22:35:27 +02:00
Alexandru Scvortov
d47565be5c fix: actually clear buffer in TestBackend::clear (#461) 2021-05-02 22:35:08 +02:00
Florian Dehau
1028d39db0 chore: improve contributing guidelines
* Improve issue templates and make them mandatory.
* Improve CONTRIBUTING.md.
* Add template for pull requests.
2021-05-02 21:42:31 +02:00
Deepu K Sasidharan
b250825c38 Add kdash to apps using this section (#469)
chore: add `kdash` to apps using `tui`
2021-05-02 20:13:56 +02:00
66 changed files with 4468 additions and 2761 deletions

View File

@@ -1,30 +1,60 @@
---
name: Bug report
about: Create a report to help us improve
about: Create an issue about a bug you encountered
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
<!--
Hi there, sorry `tui` is not working as expected.
Please fill this bug report conscientiously.
A detailed and complete issue is more likely to be processed quickly.
-->
## Description
<!--
A clear and concise description of what the bug is.
-->
**To Reproduce**
If possible include a code sample exhibiting the problem.
**Expected behavior**
## To Reproduce
<!--
Try to reduce the issue to a simple code sample exhibiting the problem.
Ideally, fork the project and add a test or an example.
-->
## Expected behavior
<!--
A clear and concise description of what you expected to happen.
-->
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Linux,Windows]
- Terminal Emulator [e.g xterm, Konsole, Terminal, iTerm2, ConEmu]
- Font [e.g Inconsolata, Monospace]
- Crate version [e.g. 0.7]
- Backend [e.g termion, crossterm]
## Screenshots
<!--
If applicable, add screenshots, gifs or videos to help explain your problem.
-->
**Additional context**
## Environment
<!--
Add a description of the systems where you are observing the issue. For example:
- OS: Linux
- Terminal Emulator: xterm
- Font: Inconsolata (Patched)
- Crate version: 0.7
- Backend: termion
-->
- OS:
- Terminal Emulator:
- Font:
- Crate version:
- Backend:
## Additional context
<!--
Add any other context about the problem here.
If you already looked into the issue, include all the leads you have explored.
-->

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -7,14 +7,26 @@ assignees: ''
---
**Is your feature request related to a problem? Please describe.**
## Problem
<!--
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
-->
**Describe the solution you'd like**
## Solution
<!--
A clear and concise description of what you want to happen.
Things to consider:
- backward compatibility
- ease of use of the API (https://rust-lang.github.io/api-guidelines/)
- consistency with the rest of the crate
-->
**Describe alternatives you've considered**
## Alternatives
<!--
A clear and concise description of any alternative solutions or features you've considered.
-->
**Additional context**
## Additional context
<!--
Add any other context or screenshots about the feature request here.
-->

17
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,17 @@
## Description
<!--
A clear and concise description of what this PR changes.
-->
## Testing guidelines
<!--
A clear and concise description of how the changes can be tested.
For example, you can include a command to run the relevant tests or examples.
You can also include screenshots of the expected behavior.
-->
## Checklist
* [ ] I have read the [contributing guidelines](../CONTRIBUTING.md).
* [ ] I have added relevant tests.
* [ ] I have documented all new additions.

View File

@@ -9,7 +9,7 @@ on:
name: CI
env:
CI_CARGO_MAKE_VERSION: 0.32.9
CI_CARGO_MAKE_VERSION: 0.35.8
jobs:
linux:
@@ -17,10 +17,8 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
rust: ["1.44.0", "stable"]
rust: ["1.56.1", "stable"]
steps:
- name: "Install dependencies"
run: sudo apt-get install libncurses5-dev
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
with:
rust-version: ${{ matrix.rust }}
@@ -38,22 +36,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.56.1", "stable"]
steps:
- uses: actions/checkout@v1
- uses: hecrj/setup-rust-action@967aec96c6a27a0ce15c1dac3aaba332d60565e2
@@ -73,13 +65,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

View File

@@ -2,6 +2,69 @@
## To be released
## v0.18.0 - 2022-04-24
### Features
* Update `crossterm` to `0.23`
## v0.17.0 - 2022-01-22
### Features
* Add option to `widgets::List` to repeat the hightlight symbol for each line of multi-line items (#533).
* Add option to control the alignment of `Axis` labels in the `Chart` widget (#568).
### Breaking changes
* The minimum supported rust version is now `1.56.1`.
#### New default backend and consolidated backend options (#553)
* `crossterm` is now the default backend.
If you are already using the `crossterm` backend, you can simplify your dependency specification in `Cargo.toml`:
```diff
- tui = { version = "0.16", default-features = false, features = ["crossterm"] }
+ tui = "0.17"
```
If you are using the `termion` backend, your `Cargo` is now a bit more verbose:
```diff
- tui = "0.16"
+ tui = { version = "0.17", default-features = false, features = ["termion"] }
```
`crossterm` has also been bumped to version `0.22`.
Because of their apparent low usage, `curses` and `rustbox` backends have been removed.
If you are using one of them, you can import their last implementation in your own project:
* [curses](https://github.com/fdehau/tui-rs/blob/v0.16.0/src/backend/curses.rs)
* [rustbox](https://github.com/fdehau/tui-rs/blob/v0.16.0/src/backend/rustbox.rs)
#### Canvas labels (#543)
* Labels of the `Canvas` widget are now `text::Spans`.
The signature of `widgets::canvas::Context::print` has thus been updated:
```diff
- ctx.print(x, y, "Some text", Color::Yellow);
+ ctx.print(x, y, Span::styled("Some text", Style::default().fg(Color::Yellow)))
```
## v0.16.0 - 2021-08-01
### Features
* Update `crossterm` to `0.20`.
* Add `From<Cow<str>>` implementation for `text::Text` (#471).
* Add option to right or center align the title of a `widgets::Block` (#462).
### Fixes
* Apply label style in `widgets::Gauge` and avoid panics because of overflows with long labels (#494).
* Avoid panics because of overflows with long axis labels in `widgets::Chart` (#512).
* Fix computation of column widths in `widgets::Table` (#514).
* Fix panics because of invalid offset when input changes between two frames in `widgets::List` and
`widgets::Chart` (#516).
## v0.15.0 - 2021-05-02
### Features

View File

@@ -1,12 +1,21 @@
# Contributing
## Building
[cargo-make]: https://github.com/sagiegurari/cargo-make "cargo-make"
`tui` is an ordinary Rust project where common tasks are managed with [cargo-make]. It wraps common
`cargo` commands with sane defaults depending on your platform of choice. Bulding the project should
be as easy as running `cargo make`.
`tui` is an ordinary Rust project where common tasks are managed with [cargo-make].
It wraps common `cargo` commands with sane defaults depending on your platform of choice.
Building the project should be as easy as running `cargo make build`.
## Continous Integration
## :hammer_and_wrench: Pull requests
All contributions are obviously welcome.
Please include as many details as possible in your PR description to help the reviewer (follow the provided template).
Make sure to highlight changes which may need additional attention or you are uncertain about.
Any idea with a large scale impact on the crate or its users should ideally be discussed in a "Feature Request" issue beforehand.
## Continuous Integration
We use Github Actions for the CI where we perform the following checks:
- The code should compile on `stable` and the Minimum Supported Rust Version (MSRV).
@@ -14,13 +23,11 @@ We use Github Actions for the CI where we perform the following checks:
- The code should conform to the default format enforced by `rustfmt`.
- The code should not contain common style issues `clippy`.
You can also check most of those things yourself locally using `cargo make ci` which will offer you
a shorter feedback loop.
You can also check most of those things yourself locally using `cargo make ci` which will offer you a shorter feedback loop.
## Tests
The test coverage of the crate is far from being ideal but we already have a fair amount of tests in
place. Beside the usal doc and unit tests, one of the most valuable test you can write for `tui` is
a test again the `TestBackend` which allows you to assert the content of the output buffer that
would have been flushed to the termminal after a given draw call (see `widgets_block_renders` in
[tests/widgets_block.rs](./tests/widget_block.rs) for an example).
The test coverage of the crate is far from being ideal but we already have a fair amount of tests in place.
Beside the usual doc and unit tests, one of the most valuable test you can write for `tui` is a test again the `TestBackend`.
It allows you to assert the content of the output buffer that would have been flushed to the terminal after a given draw call.
See `widgets_block_renders` in [tests/widgets_block.rs](./tests/widget_block.rs) for an example.

View File

@@ -1,127 +1,93 @@
[package]
name = "tui"
version = "0.15.0"
version = "0.18.0"
authors = ["Florian Dehau <work@fdehau.com>"]
description = """
A library to build rich terminal user interfaces or dashboards
"""
documentation = "https://docs.rs/tui/0.15.0/tui/"
documentation = "https://docs.rs/tui/0.18.0/tui/"
keywords = ["tui", "terminal", "dashboard"]
repository = "https://github.com/fdehau/tui-rs"
readme = "README.md"
license = "MIT"
exclude = ["assets/*", ".github", "Makefile.toml", "CONTRIBUTING.md", "*.log", "tags"]
autoexamples = true
edition = "2018"
edition = "2021"
[badges]
[features]
default = ["termion"]
curses = ["easycurses", "pancurses"]
default = ["crossterm"]
[dependencies]
bitflags = "1.0"
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.19", 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.23", optional = true }
serde = { version = "1", optional = true, features = ["derive"]}
[dev-dependencies]
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 = "panic"
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"]

View File

@@ -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
'''

View File

@@ -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,58 +28,53 @@ 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.56.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.
The UI code is in [examples/demo/ui.rs](examples/demo/ui.rs) while the
application state is in [examples/demo/app.rs](examples/demo/app.rs).
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
```
The UI code is in [examples/demo/ui.rs](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/demo/ui.rs) while the
application state is in [examples/demo/app.rs](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/demo/app.rs).
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
The library comes with the following list of widgets:
* [Block](examples/block.rs)
* [Gauge](examples/gauge.rs)
* [Sparkline](examples/sparkline.rs)
* [Chart](examples/chart.rs)
* [BarChart](examples/barchart.rs)
* [List](examples/list.rs)
* [Table](examples/table.rs)
* [Paragraph](examples/paragraph.rs)
* [Canvas (with line, point cloud, map)](examples/canvas.rs)
* [Tabs](examples/tabs.rs)
* [Block](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/block.rs)
* [Gauge](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/gauge.rs)
* [Sparkline](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/sparkline.rs)
* [Chart](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/chart.rs)
* [BarChart](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/barchart.rs)
* [List](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/list.rs)
* [Table](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/table.rs)
* [Paragraph](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/paragraph.rs)
* [Canvas (with line, point cloud, map)](https://github.com/fdehau/tui-rs/blob/v0.18.0/examples/canvas.rs)
* [Tabs](https://github.com/fdehau/tui-rs/blob/v0.18.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
@@ -96,6 +85,7 @@ You can run all examples by running `make run-examples`.
* [spotify-tui](https://github.com/Rigellute/spotify-tui)
* [bandwhich](https://github.com/imsnif/bandwhich)
* [kmon](https://github.com/orhun/kmon)
* [gpg-tui](https://github.com/orhun/gpg-tui)
* [ytop](https://github.com/cjbassi/ytop)
* [zenith](https://github.com/bvaisvil/zenith)
* [bottom](https://github.com/ClementTsang/bottom)
@@ -110,6 +100,21 @@ You can run all examples by running `make run-examples`.
* [taskwarrior-tui](https://github.com/kdheepak/taskwarrior-tui)
* [gping](https://github.com/orf/gping/)
* [Vector](https://vector.dev)
* [KDash](https://github.com/kdash-rs/kdash)
* [xplr](https://github.com/sayanarijit/xplr)
* [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)
* [tokio-console](https://github.com/tokio-rs/console): a diagnostics and debugging tool for asynchronous Rust programs.
* [hwatch](https://github.com/blacknon/hwatch): a alternative watch command that records the result of command execution and can display its history and diffs.
* [ytui-music](https://github.com/sudipghimire533/ytui-music): listen to music from youtube inside your terminal.
* [mqttui](https://github.com/EdJoPaTo/mqttui): subscribe or publish to a MQTT Topic quickly from the terminal.
* [meteo-tui](https://github.com/16arpi/meteo-tui): french weather via the command line.
* [picterm](https://github.com/ksk001100/picterm): preview images in your terminal.
* [gobang](https://github.com/TaKO8Ki/gobang): a cross-platform TUI database management tool.
### Alternatives

View File

@@ -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]);
}

View File

@@ -1,86 +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,
layout::{Constraint, Direction, Layout},
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();
let block = Block::default()
.borders(Borders::ALL)
.title("Main block with round corners")
.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());
terminal.draw(ui)?;
let top_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[0]);
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]);
let block = Block::default().title(Span::styled(
"Styled title",
Style::default()
.fg(Color::White)
.bg(Color::Red)
.add_modifier(Modifier::BOLD),
));
f.render_widget(block, top_chunks[1]);
let bottom_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]);
let block = Block::default().title("With borders").borders(Borders::ALL);
f.render_widget(block, bottom_chunks[0]);
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]);
}

View File

@@ -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]);
}

View File

@@ -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]);
}

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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);
}

View File

@@ -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>,

View 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
View 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(())
}

View File

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

80
examples/demo/termion.rs Normal file
View 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
}

View File

@@ -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 {

View File

@@ -1,15 +1,20 @@
#[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},
text::Span,
widgets::{Block, Borders, Gauge},
Terminal,
Frame, Terminal,
};
struct App {
@@ -24,17 +29,17 @@ impl App {
App {
progress1: 0,
progress2: 0,
progress3: 0.0,
progress3: 0.45,
progress4: 0,
}
}
fn update(&mut self) {
self.progress1 += 5;
fn on_tick(&mut self) {
self.progress1 += 1;
if self.progress1 > 100 {
self.progress1 = 0;
}
self.progress2 += 10;
self.progress2 += 2;
if self.progress2 > 100 {
self.progress2 = 0;
}
@@ -42,7 +47,7 @@ impl App {
if self.progress3 > 1.0 {
self.progress3 = 0.0;
}
self.progress4 += 3;
self.progress4 += 1;
if self.progress4 > 100 {
self.progress4 = 0;
}
@@ -50,78 +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::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 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 gauge = Gauge::default()
.block(Block::default().title("Gauge3").borders(Borders::ALL))
.gauge_style(Style::default().fg(Color::Yellow))
.ratio(app.progress3);
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)
.use_unicode(true);
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]);
}

View File

@@ -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]);
}

View File

@@ -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 exapmle 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 it's 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]);
}

142
examples/panic.rs Normal file
View File

@@ -0,0 +1,142 @@
//! How to use a panic hook to reset the terminal before printing the panic to
//! the terminal.
//!
//! When exiting normally or when handling `Result::Err`, we can reset the
//! terminal manually at the end of `main` just before we print the error.
//!
//! Because a panic interrupts the normal control flow, manually resetting the
//! terminal at the end of `main` won't do us any good. Instead, we need to
//! make sure to set up a panic hook that first resets the terminal before
//! handling the panic. This both reuses the standard panic hook to ensure a
//! consistent panic handling UX and properly resets the terminal to not
//! distort the output.
//!
//! That's why this example is set up to show both situations, with and without
//! the chained panic hook, to see the difference.
#![deny(clippy::all)]
#![warn(clippy::pedantic, clippy::nursery)]
use std::error::Error;
use std::io;
use crossterm::event::{self, Event, KeyCode};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
use tui::backend::{Backend, CrosstermBackend};
use tui::layout::Alignment;
use tui::text::Spans;
use tui::widgets::{Block, Borders, Paragraph};
use tui::{Frame, Terminal};
type Result<T> = std::result::Result<T, Box<dyn Error>>;
#[derive(Default)]
struct App {
hook_enabled: bool,
}
impl App {
fn chain_hook(&mut self) {
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic| {
reset_terminal().unwrap();
original_hook(panic);
}));
self.hook_enabled = true;
}
}
fn main() -> Result<()> {
let mut terminal = init_terminal()?;
let mut app = App::default();
let res = run_tui(&mut terminal, &mut app);
reset_terminal()?;
if let Err(err) = res {
println!("{:?}", err);
}
Ok(())
}
/// Initializes the terminal.
fn init_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
crossterm::execute!(io::stdout(), EnterAlternateScreen)?;
enable_raw_mode()?;
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
Ok(terminal)
}
/// Resets the terminal.
fn reset_terminal() -> Result<()> {
disable_raw_mode()?;
crossterm::execute!(io::stdout(), LeaveAlternateScreen)?;
Ok(())
}
/// Runs the TUI loop.
fn run_tui<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f, app))?;
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('p') => {
panic!("intentional demo panic");
}
KeyCode::Char('e') => {
app.chain_hook();
}
_ => {
return Ok(());
}
}
}
}
}
/// Render the TUI.
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let text = vec![
if app.hook_enabled {
Spans::from("HOOK IS CURRENTLY **ENABLED**")
} else {
Spans::from("HOOK IS CURRENTLY **DISABLED**")
},
Spans::from(""),
Spans::from("press `p` to panic"),
Spans::from("press `e` to enable the terminal-resetting panic hook"),
Spans::from("press any other key to quit without panic"),
Spans::from(""),
Spans::from("when you panic without the chained hook,"),
Spans::from("you will likely have to reset your terminal afterwards"),
Spans::from("with the `reset` command"),
Spans::from(""),
Spans::from("with the chained panic hook enabled,"),
Spans::from("you should see the panic report as you would without tui"),
Spans::from(""),
Spans::from("try first without the panic handler to see the difference"),
];
let b = Block::default()
.title("Panic Handler Demo")
.borders(Borders::ALL);
let p = Paragraph::new(text).block(b).alignment(Alignment::Center);
f.render_widget(p, f.size());
}

View File

@@ -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]);
}

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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]);
}

View File

@@ -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);
}

View File

@@ -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]);
}

View File

@@ -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(())
}

245
examples/text_input.rs Normal file
View File

@@ -0,0 +1,245 @@
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{error::Error, io};
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans},
widgets::{
Block, Borders, Cell, InteractiveWidgetState, List, ListItem, Paragraph, Row, Table, TextInput,
TextInputState,
},
Frame, Terminal,
};
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 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(())
}
const NUM_INPUTS: usize = 3;
#[derive(Default)]
struct App {
input_states: [TextInputState; NUM_INPUTS],
focused_input_idx: Option<usize>,
events: Vec<Event>,
}
impl App {
fn focus_next(&mut self) {
self.focused_input_idx = match self.focused_input_idx {
Some(idx) => {
if idx == (NUM_INPUTS - 1) {
None
} else {
Some(idx + 1)
}
}
None => Some(0),
};
self.set_focused();
}
fn focus_prev(&mut self) {
self.focused_input_idx = match self.focused_input_idx {
Some(idx) => {
if idx == 0 {
None
} else {
Some(idx - 1)
}
}
None => Some(NUM_INPUTS - 1),
};
self.set_focused();
}
fn set_focused(&mut self) {
for input_state in self.input_states.iter_mut() {
input_state.unfocus();
}
if let Some(idx) = self.focused_input_idx {
self.input_states[idx].focus();
}
}
fn focused_input_mut(&mut self) -> Option<&mut TextInputState> {
if let Some(idx) = self.focused_input_idx {
Some(&mut self.input_states[idx])
} else {
None
}
}
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
let mut app = App::default();
loop {
terminal.draw(|f| ui(f, &mut app))?;
let event = event::read()?;
app.events.push(event);
if let Some(state) = app.focused_input_mut() {
if state.handle_event(event).is_consumed() {
continue;
}
}
match event {
Event::Key(key) => match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Tab => app.focus_next(),
KeyCode::BackTab => app.focus_prev(),
_ => {}
},
_ => {}
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let layout = Layout::default()
.horizontal_margin(10)
.vertical_margin(2)
.constraints(
[
Constraint::Length(10),
Constraint::Length(14),
Constraint::Length(5),
Constraint::Percentage(100),
]
.as_ref(),
)
.split(f.size());
let info_block = Paragraph::new(vec![
Spans::from(Span::raw("Press 'TAB' to go to the next input")),
Spans::from(Span::raw("Press 'SHIFT+TAB' to go to the previous input")),
Spans::from(Span::raw("Press 'q' to quit when no input is focused")),
Spans::from(Span::raw(
"Supports a subset of readline keyboard shortcuts:",
)),
Spans::from(Span::raw(
" - ctrl+e / ctrl+a to jump to text input end / start",
)),
Spans::from(Span::raw(
" - ctrl+w delete to the start of the current word",
)),
Spans::from(Span::raw(
" - alt+b / alt+f to jump backwards / forwards a word",
)),
Spans::from(Span::raw(" - left / right arrow keys to move the cursor")),
])
.block(Block::default().title("Information").borders(Borders::ALL));
f.render_widget(info_block, layout[0]);
let inputs_block = Block::default().title("Inputs").borders(Borders::ALL);
let inputs_rect = inputs_block.inner(layout[1]);
f.render_widget(inputs_block, layout[1]);
let inputs_layout = Layout::default()
.constraints(
[
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
]
.as_ref(),
)
.split(inputs_rect);
{
let text_input =
TextInput::new().block(Block::default().title("Basic Input").borders(Borders::ALL));
f.render_interactive(text_input, inputs_layout[0], &mut app.input_states[0]);
}
{
let text_input = TextInput::new()
.block(
Block::default()
.title("Has Placeholder")
.borders(Borders::ALL),
)
.placeholder_text("Type something...");
f.render_interactive(text_input, inputs_layout[1], &mut app.input_states[1]);
}
{
let text_input = TextInput::new()
.text_style(Style::default().fg(Color::Yellow))
.block(Block::default().title("Is Followed").borders(Borders::ALL));
f.render_interactive(text_input, inputs_layout[2], &mut app.input_states[2]);
}
{
let text_input = TextInput::new()
.read_only(true)
.text_style(Style::default().fg(Color::LightBlue))
.block(
Block::default()
.title("Follows Above (read only)")
.borders(Borders::ALL),
);
f.render_interactive(text_input, inputs_layout[3], &mut app.input_states[2]);
}
let table = Table::new(
app.input_states
.iter()
.enumerate()
.map(|(idx, input_state)| {
Row::new(vec![
Cell::from(Span::raw(format!("Input {}", idx + 1))),
Cell::from(Span::styled(
input_state.get_value(),
Style::default().add_modifier(Modifier::BOLD),
)),
])
})
.collect::<Vec<_>>(),
)
.widths(&[Constraint::Min(10), Constraint::Percentage(100)])
.block(Block::default().title("Input Values").borders(Borders::ALL));
f.render_widget(table, layout[2]);
let events = List::new(
app.events
.iter()
.rev()
.map(|event| ListItem::new(Span::raw(format!("{:?}", event))))
.collect::<Vec<_>>(),
)
.block(Block::default().title("Events").borders(Borders::ALL));
f.render_widget(events, layout[3]);
}

View File

@@ -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,130 +51,142 @@ 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 mut 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;
events.disable_exit_key();
}
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;
events.enable_exit_key();
}
_ => {}
},
}
}
}
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]);
}

View File

@@ -1,95 +0,0 @@
use std::io;
use std::sync::mpsc;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
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<()>,
ignore_exit_key: Arc<AtomicBool>,
tick_handle: thread::JoinHandle<()>,
}
#[derive(Debug, Clone, Copy)]
pub struct Config {
pub exit_key: Key,
pub tick_rate: Duration,
}
impl Default for Config {
fn default() -> Config {
Config {
exit_key: Key::Char('q'),
tick_rate: Duration::from_millis(250),
}
}
}
impl Events {
pub fn new() -> Events {
Events::with_config(Config::default())
}
pub fn with_config(config: Config) -> Events {
let (tx, rx) = mpsc::channel();
let ignore_exit_key = Arc::new(AtomicBool::new(false));
let input_handle = {
let tx = tx.clone();
let ignore_exit_key = ignore_exit_key.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;
}
if !ignore_exit_key.load(Ordering::Relaxed) && key == config.exit_key {
return;
}
}
}
})
};
let tick_handle = {
thread::spawn(move || loop {
if tx.send(Event::Tick).is_err() {
break;
}
thread::sleep(config.tick_rate);
})
};
Events {
rx,
ignore_exit_key,
input_handle,
tick_handle,
}
}
pub fn next(&self) -> Result<Event<Key>, mpsc::RecvError> {
self.rx.recv()
}
pub fn disable_exit_key(&mut self) {
self.ignore_exit_key.store(true, Ordering::Relaxed);
}
pub fn enable_exit_key(&mut self) {
self.ignore_exit_key.store(false, Ordering::Relaxed);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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,11 +13,6 @@ 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;

View File

@@ -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 Into<rustbox::Color> for Color {
fn into(self) -> rustbox::Color {
match self {
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 Into<rustbox::Style> for Modifier {
fn into(self) -> rustbox::Style {
let mut result = rustbox::Style::empty();
if self.contains(Modifier::BOLD) {
result.insert(rustbox::RB_BOLD);
}
if self.contains(Modifier::UNDERLINED) {
result.insert(rustbox::RB_UNDERLINE);
}
if self.contains(Modifier::REVERSED) {
result.insert(rustbox::RB_REVERSE);
}
result
}
}

View File

@@ -137,6 +137,7 @@ impl Backend for TestBackend {
}
fn clear(&mut self) -> Result<(), io::Error> {
self.buffer.reset();
Ok(())
}

View File

@@ -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 {

View File

@@ -63,6 +63,9 @@ pub struct Layout {
direction: Direction,
margin: Margin,
constraints: Vec<Constraint>,
/// Whether the last chunk of the computed layout should be expanded to fill the available
/// space.
expand_to_fill: bool,
}
thread_local! {
@@ -78,6 +81,7 @@ impl Default for Layout {
vertical: 0,
},
constraints: Vec::new(),
expand_to_fill: true,
}
}
}
@@ -114,6 +118,11 @@ impl Layout {
self
}
pub(crate) fn expand_to_fill(mut self, expand_to_fill: bool) -> Layout {
self.expand_to_fill = expand_to_fill;
self
}
/// Wrapper function around the cassowary-rs solver to be able to split a given
/// area into smaller ones based on the preferred widths or heights and the direction.
///
@@ -222,11 +231,13 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
Direction::Vertical => first.top() | EQ(REQUIRED) | f64::from(dest_area.top()),
});
}
if let Some(last) = elements.last() {
ccs.push(match layout.direction {
Direction::Horizontal => last.right() | EQ(REQUIRED) | f64::from(dest_area.right()),
Direction::Vertical => last.bottom() | EQ(REQUIRED) | f64::from(dest_area.bottom()),
});
if layout.expand_to_fill {
if let Some(last) = elements.last() {
ccs.push(match layout.direction {
Direction::Horizontal => last.right() | EQ(REQUIRED) | f64::from(dest_area.right()),
Direction::Vertical => last.bottom() | EQ(REQUIRED) | f64::from(dest_area.bottom()),
});
}
}
match layout.direction {
Direction::Horizontal => {
@@ -299,14 +310,16 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
}
}
// Fix imprecision by extending the last item a bit if necessary
if let Some(last) = results.last_mut() {
match layout.direction {
Direction::Vertical => {
last.height = dest_area.bottom() - last.y;
}
Direction::Horizontal => {
last.width = dest_area.right() - last.x;
if layout.expand_to_fill {
// Fix imprecision by extending the last item a bit if necessary
if let Some(last) = results.last_mut() {
match layout.direction {
Direction::Vertical => {
last.height = dest_area.bottom() - last.y;
}
Direction::Horizontal => {
last.width = dest_area.right() - last.x;
}
}
}
}
@@ -350,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,
@@ -358,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.

View File

@@ -9,18 +9,19 @@
//!
//! ```toml
//! [dependencies]
//! tui = "0.15"
//! termion = "1.5"
//! tui = "0.18"
//! crossterm = "0.23"
//! ```
//!
//! 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.19"
//! tui = { version = "0.15", default-features = false, features = ['crossterm'] }
//! termion = "1.5"
//! tui = { version = "0.18", default-features = false, features = ['termion'] }
//!
//! ```
//!
//! The same logic applies for all other available backends.
@@ -33,29 +34,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(())
//! }
@@ -71,23 +70,33 @@
//! implement your own.
//!
//! Each widget follows a builder pattern API providing a default configuration along with methods
//! to customize them. The widget is then rendered using the [`Frame::render_widget`] which take
//! your widget instance an area to draw to.
//! to customize them. The widget is then rendered using [`Frame::render_widget`] which takes
//! your widget instance and an area to draw to.
//!
//! 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 +104,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 +127,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]);
//! }
//! ```
//!

View File

@@ -2,7 +2,7 @@ use crate::{
backend::Backend,
buffer::Buffer,
layout::Rect,
widgets::{StatefulWidget, Widget},
widgets::{InteractiveWidget, StatefulWidget, Widget},
};
use std::io;
@@ -82,14 +82,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 +108,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));
@@ -137,6 +133,20 @@ where
widget.render(area, self.terminal.current_buffer_mut(), state);
}
pub fn render_interactive<W>(&mut self, widget: W, area: Rect, state: &W::State)
where
W: InteractiveWidget,
{
widget.render(area, self, state);
}
pub fn render_interactive_mut<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
where
W: InteractiveWidget,
{
widget.render_mut(area, self, state);
}
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
/// coordinates. If this method is not called, the cursor will be hidden.
///

View File

@@ -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.
///
@@ -398,6 +386,12 @@ impl<'a> From<&'a str> for Text<'a> {
}
}
impl<'a> From<Cow<'a, str>> for Text<'a> {
fn from(s: Cow<'a, str>) -> Text<'a> {
Text::raw(s)
}
}
impl<'a> From<Span<'a>> for Text<'a> {
fn from(span: Span<'a>) -> Text<'a> {
Text {

View File

@@ -1,6 +1,6 @@
use crate::{
buffer::Buffer,
layout::Rect,
layout::{Alignment, Rect},
style::Style,
symbols::line,
text::{Span, Spans},
@@ -45,6 +45,9 @@ impl BorderType {
pub struct Block<'a> {
/// Optional title place on the upper left of the block
title: Option<Spans<'a>>,
/// Title alignment. The default is top left of the block, but one can choose to place
/// title in the top middle, or top right of the block
title_alignment: Alignment,
/// Visible borders
borders: Borders,
/// Border style
@@ -60,6 +63,7 @@ impl<'a> Default for Block<'a> {
fn default() -> Block<'a> {
Block {
title: None,
title_alignment: Alignment::Left,
borders: Borders::NONE,
border_style: Default::default(),
border_type: BorderType::Plain,
@@ -89,6 +93,11 @@ impl<'a> Block<'a> {
self
}
pub fn title_alignment(mut self, alignment: Alignment) -> Block<'a> {
self.title_alignment = alignment;
self
}
pub fn border_style(mut self, style: Style) -> Block<'a> {
self.border_style = style;
self
@@ -192,19 +201,38 @@ impl<'a> Widget for Block<'a> {
.set_style(self.border_style);
}
// Title
if let Some(title) = self.title {
let lx = if self.borders.intersects(Borders::LEFT) {
let left_border_dx = if self.borders.intersects(Borders::LEFT) {
1
} else {
0
};
let rx = if self.borders.intersects(Borders::RIGHT) {
let right_border_dx = if self.borders.intersects(Borders::RIGHT) {
1
} else {
0
};
let width = area.width.saturating_sub(lx).saturating_sub(rx);
buf.set_spans(area.left() + lx, area.top(), &title, width);
let title_area_width = area
.width
.saturating_sub(left_border_dx)
.saturating_sub(right_border_dx);
let title_dx = match self.title_alignment {
Alignment::Left => left_border_dx,
Alignment::Center => area.width.saturating_sub(title.width() as u16) / 2,
Alignment::Right => area
.width
.saturating_sub(title.width() as u16)
.saturating_sub(right_border_dx),
};
let title_x = area.left() + title_dx;
let title_y = area.top();
buf.set_spans(title_x, title_y, &title, title_area_width);
}
}
}
@@ -507,5 +535,39 @@ mod tests {
height: 0,
},
);
assert_eq!(
Block::default()
.title("Test")
.title_alignment(Alignment::Center)
.inner(Rect {
x: 0,
y: 0,
width: 0,
height: 1,
}),
Rect {
x: 0,
y: 1,
width: 0,
height: 0,
},
);
assert_eq!(
Block::default()
.title("Test")
.title_alignment(Alignment::Right)
.inner(Rect {
x: 0,
y: 0,
width: 0,
height: 1,
}),
Rect {
x: 0,
y: 1,
width: 0,
height: 0,
},
);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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),

View File

@@ -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
@@ -296,18 +296,8 @@ impl<'a> Chart<'a> {
y -= 1;
}
if let Some(ref y_labels) = self.y_axis.labels {
let mut max_width = y_labels.iter().map(Span::width).max().unwrap_or_default() as u16;
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 x + max_width < area.right() {
layout.label_y = Some(x);
x += max_width;
}
}
layout.label_y = self.y_axis.labels.as_ref().and(Some(x));
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);
@@ -362,6 +352,143 @@ impl<'a> Chart<'a> {
}
layout
}
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(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)
}
fn render_x_labels(
&mut self,
buf: &mut Buffer,
layout: &ChartLayout,
chart_area: Rect,
graph_area: Rect,
) {
let y = match layout.label_x {
Some(y) => y,
None => return,
};
let labels = self.x_axis.labels.as_ref().unwrap();
let labels_len = labels.len() as u16;
if labels_len < 2 {
return;
}
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(
&mut self,
buf: &mut Buffer,
layout: &ChartLayout,
chart_area: Rect,
graph_area: Rect,
) {
let x = match layout.label_y {
Some(x) => x,
None => return,
};
let labels = self.y_axis.labels.as_ref().unwrap();
let labels_len = labels.len() as u16;
for (i, label) in labels.iter().enumerate() {
let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1);
if dy < graph_area.bottom() {
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);
}
}
}
}
impl<'a> Widget for Chart<'a> {
@@ -390,33 +517,8 @@ impl<'a> Widget for Chart<'a> {
return;
}
if let Some(y) = layout.label_x {
let labels = self.x_axis.labels.unwrap();
let total_width = labels.iter().map(Span::width).sum::<usize>() as u16;
let labels_len = labels.len() as u16;
if total_width < graph_area.width && labels_len > 1 {
for (i, label) in labels.iter().enumerate() {
buf.set_span(
graph_area.left() + i as u16 * (graph_area.width - 1) / (labels_len - 1)
- label.content.width() as u16,
y,
label,
label.width() as u16,
);
}
}
}
if let Some(x) = layout.label_y {
let labels = self.y_axis.labels.unwrap();
let labels_len = labels.len() as u16;
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);
}
}
}
self.render_x_labels(buf, &layout, chart_area, graph_area);
self.render_y_labels(buf, &layout, chart_area, graph_area);
if let Some(y) = layout.axis_x {
for x in graph_area.left()..graph_area.right() {

View File

@@ -1,8 +1,9 @@
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::widgets::Widget;
use crate::{buffer::Buffer, layout::Rect, widgets::Widget};
/// A widget to to clear/reset a certain area to allow overdrawing (e.g. for popups)
/// A widget to clear/reset a certain area to allow overdrawing (e.g. for popups).
///
/// This widget **cannot be used to clear the terminal on the first render** as `tui` assumes the
/// render area is empty. Use [`crate::Terminal::clear`] instead.
///
/// # Examples
///

View File

@@ -0,0 +1,25 @@
use crossterm::event::Event;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum InteractionOutcome {
Consumed,
Bubble,
}
impl InteractionOutcome {
pub fn is_consumed(&self) -> bool {
matches!(self, InteractionOutcome::Consumed)
}
pub fn is_bubble(&self) -> bool {
matches!(self, InteractionOutcome::Bubble)
}
}
pub trait InteractiveWidgetState {
fn handle_event(&mut self, _event: Event) -> InteractionOutcome {
InteractionOutcome::Bubble
}
fn is_focused(&self) -> bool;
fn focus(&mut self);
fn unfocus(&mut self);
}

View File

@@ -60,7 +60,7 @@ impl<'a> Gauge<'a> {
/// Sets ratio ([0.0, 1.0]) directly.
pub fn ratio(mut self, ratio: f64) -> Gauge<'a> {
assert!(
ratio <= 1.0 && ratio >= 0.0,
(0.0..=1.0).contains(&ratio),
"Ratio should be between 0 and 1 inclusively."
);
self.ratio = ratio;
@@ -107,56 +107,45 @@ impl<'a> Widget for Gauge<'a> {
return;
}
let center = gauge_area.height / 2 + gauge_area.top();
let width = f64::from(gauge_area.width) * self.ratio;
//go to regular rounding behavior if we're not using unicode blocks
let end = gauge_area.left()
+ if self.use_unicode {
width.floor() as u16
} else {
width.round() as u16
};
// Label
let ratio = self.ratio;
let label = self
.label
.unwrap_or_else(|| Span::from(format!("{}%", (ratio * 100.0).round())));
// compute label value and its position
// label is put at the center of the gauge_area
let label = {
let pct = f64::round(self.ratio * 100.0);
self.label
.unwrap_or_else(|| Span::from(format!("{}%", pct)))
};
let clamped_label_width = gauge_area.width.min(label.width() as u16);
let label_col = gauge_area.left() + (gauge_area.width - clamped_label_width) / 2;
let label_row = gauge_area.top() + gauge_area.height / 2;
// the gauge will be filled proportionally to the ratio
let filled_width = f64::from(gauge_area.width) * self.ratio;
let end = if self.use_unicode {
gauge_area.left() + filled_width.floor() as u16
} else {
gauge_area.left() + filled_width.round() as u16
};
for y in gauge_area.top()..gauge_area.bottom() {
// Gauge
// render the filled area (left to end)
for x in gauge_area.left()..end {
buf.get_mut(x, y).set_symbol(" ");
}
//set unicode block
if self.use_unicode && self.ratio < 1.0 {
buf.get_mut(end, y)
.set_symbol(get_unicode_block(width % 1.0));
}
let mut color_end = end;
if y == center {
let label_width = label.width() as u16;
let middle = (gauge_area.width - label_width) / 2 + gauge_area.left();
buf.set_span(middle, y, &label, gauge_area.right() - middle);
if self.use_unicode && end >= middle && end < middle + label_width {
color_end = gauge_area.left() + (width.round() as u16); //set color on the label to the rounded gauge level
}
}
// Fix colors
for x in gauge_area.left()..color_end {
// spaces are needed to apply the background styling
buf.get_mut(x, y)
.set_symbol(" ")
.set_fg(self.gauge_style.bg.unwrap_or(Color::Reset))
.set_bg(self.gauge_style.fg.unwrap_or(Color::Reset));
}
if self.use_unicode && self.ratio < 1.0 {
buf.get_mut(end, y)
.set_symbol(get_unicode_block(filled_width % 1.0));
}
}
// set the span
buf.set_span(label_col, label_row, &label, clamped_label_width);
}
}
fn get_unicode_block<'a>(frac: f64) -> &'a str {
match (frac * 8.0).round() as u16 {
//get how many eighths the fraction is closest to
1 => symbols::block::ONE_EIGHTH,
2 => symbols::block::ONE_QUARTER,
3 => symbols::block::THREE_EIGHTHS,
@@ -213,7 +202,7 @@ impl<'a> LineGauge<'a> {
pub fn ratio(mut self, ratio: f64) -> Self {
assert!(
ratio <= 1.0 && ratio >= 0.0,
(0.0..=1.0).contains(&ratio),
"Ratio should be between 0 and 1 inclusively."
);
self.ratio = ratio;

View File

@@ -5,24 +5,14 @@ use crate::{
text::Text,
widgets::{Block, StatefulWidget, Widget},
};
use std::iter::{self, Iterator};
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
@@ -88,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> {
@@ -102,6 +94,7 @@ impl<'a> List<'a> {
start_corner: Corner::TopLeft,
highlight_style: Style::default(),
highlight_symbol: None,
repeat_highlight_symbol: false,
}
}
@@ -125,10 +118,53 @@ 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
}
fn get_items_bounds(
&self,
selected: Option<usize>,
offset: usize,
max_height: usize,
) -> (usize, usize) {
let offset = offset.min(self.items.len().saturating_sub(1));
let mut start = offset;
let mut end = offset;
let mut height = 0;
for item in self.items.iter().skip(offset) {
if height + item.height() > max_height {
break;
}
height += item.height();
end += 1;
}
let selected = selected.unwrap_or(0).min(self.items.len() - 1);
while selected >= end {
height = height.saturating_add(self.items[end].height());
end += 1;
while height > max_height {
height = height.saturating_sub(self.items[start].height());
start += 1;
}
}
while selected < start {
start -= 1;
height = height.saturating_add(self.items[start].height());
while height > max_height {
end -= 1;
height = height.saturating_sub(self.items[end].height());
}
}
(start, end)
}
}
impl<'a> StatefulWidget for List<'a> {
@@ -154,40 +190,11 @@ impl<'a> StatefulWidget for List<'a> {
}
let list_height = list_area.height as usize;
let mut start = state.offset;
let mut end = state.offset;
let mut height = 0;
for item in self.items.iter().skip(state.offset) {
if height + item.height() > list_height {
break;
}
height += item.height();
end += 1;
}
let selected = state.selected.unwrap_or(0).min(self.items.len() - 1);
while selected >= end {
height = height.saturating_add(self.items[end].height());
end += 1;
while height > list_height {
height = height.saturating_sub(self.items[start].height());
start += 1;
}
}
while selected < start {
start -= 1;
height = height.saturating_add(self.items[start].height());
while height > list_height {
end -= 1;
height = height.saturating_sub(self.items[end].height());
}
}
let (start, end) = self.get_items_bounds(state.selected, state.offset, list_height);
state.offset = start;
let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = iter::repeat(" ")
.take(highlight_symbol.width())
.collect::<String>();
let blank_symbol = " ".repeat(highlight_symbol.width());
let mut current_height = 0;
let has_selection = state.selected.is_some();
@@ -219,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 {

View File

@@ -27,6 +27,10 @@ mod reflow;
mod sparkline;
mod table;
mod tabs;
mod text_input;
#[cfg(feature = "crossterm")]
mod crossterm_interactive_widget;
pub use self::barchart::BarChart;
pub use self::block::{Block, BorderType};
@@ -38,7 +42,13 @@ pub use self::paragraph::{Paragraph, Wrap};
pub use self::sparkline::Sparkline;
pub use self::table::{Cell, Row, Table, TableState};
pub use self::tabs::Tabs;
pub use self::text_input::{TextInput, TextInputState};
#[cfg(feature = "crossterm")]
pub use self::crossterm_interactive_widget::{InteractiveWidgetState, InteractionOutcome};
use crate::backend::Backend;
use crate::Frame;
use crate::{buffer::Buffer, layout::Rect};
use bitflags::bitflags;
@@ -62,8 +72,8 @@ bitflags! {
/// Base requirements for a Widget
pub trait Widget {
/// Draws the current state of the widget in the given buffer. That the only method required to
/// implement a custom widget.
/// Draws the current state of the widget in the given buffer. That is the only method required
/// to implement a custom widget.
fn render(self, area: Rect, buf: &mut Buffer);
}
@@ -87,7 +97,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 +164,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"),
@@ -183,3 +192,21 @@ pub trait StatefulWidget {
type State;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
}
pub trait InteractiveWidget {
type State;
fn render<'a, B: Backend + 'a>(
self,
area: Rect,
frame: &mut Frame<'a, B>,
state: &Self::State,
);
fn render_mut<'a, B: Backend + 'a>(
self,
area: Rect,
frame: &mut Frame<'a, B>,
state: &mut Self::State,
);
}

View File

@@ -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![
"コンピュータ上で文字",
@@ -489,7 +489,7 @@ mod test {
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAA", "AAAA\u{00a0}AAA",]);
// Ensure that if the character was a regular space, it would be wrapped differently.
let text_space = text.replace("\u{00a0}", " ");
let text_space = text.replace('\u{00a0}', " ");
let (word_wrapper_space, _) =
run_composer(Composer::WordWrapper { trim: true }, &text_space, width);
assert_eq!(word_wrapper_space, vec!["AAAAAAAAAAAAAAA AAAA", "AAA",]);

View File

@@ -1,19 +1,10 @@
use crate::{
buffer::Buffer,
layout::{Constraint, Rect},
layout::{Constraint, Direction, Layout, Rect},
style::Style,
text::Text,
widgets::{Block, StatefulWidget, Widget},
};
use cassowary::{
strength::{MEDIUM, REQUIRED, WEAK},
WeightedRelation::*,
{Expression, Solver},
};
use std::{
collections::HashMap,
iter::{self, Iterator},
};
use unicode_width::UnicodeWidthStr;
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
@@ -23,6 +14,7 @@ use unicode_width::UnicodeWidthStr;
/// # use tui::widgets::Cell;
/// # use tui::style::{Style, Modifier};
/// # use tui::text::{Span, Spans, Text};
/// # use std::borrow::Cow;
/// Cell::from("simple string");
///
/// Cell::from(Span::from("span"));
@@ -33,6 +25,8 @@ use unicode_width::UnicodeWidthStr;
/// ]));
///
/// Cell::from(Text::from("a text"));
///
/// Cell::from(Text::from(Cow::Borrowed("hello")));
/// ```
///
/// You can apply a [`Style`] on the entire [`Cell`] using [`Cell::style`] or rely on the styling
@@ -81,6 +75,16 @@ where
/// ]);
/// ```
///
/// You can also construct a row from any type that can be converted into [`Text`]:
/// ```rust
/// # use std::borrow::Cow;
/// # use tui::widgets::Row;
/// Row::new(vec![
/// Cow::Borrowed("hello"),
/// Cow::Owned("world".to_uppercase()),
/// ]);
/// ```
///
/// By default, a row has a height of 1 but you can change this using [`Row::height`].
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Row<'a> {
@@ -263,69 +267,33 @@ impl<'a> Table<'a> {
}
fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec<u16> {
let mut solver = Solver::new();
let mut var_indices = HashMap::new();
let mut ccs = Vec::new();
let mut variables = Vec::new();
for i in 0..self.widths.len() {
let var = cassowary::Variable::new();
variables.push(var);
var_indices.insert(var, i);
}
let spacing_width = (variables.len() as u16).saturating_sub(1) * self.column_spacing;
let mut available_width = max_width.saturating_sub(spacing_width);
let mut constraints = Vec::with_capacity(self.widths.len() * 2 + 1);
if has_selection {
let highlight_symbol_width =
self.highlight_symbol.map(|s| s.width() as u16).unwrap_or(0);
available_width = available_width.saturating_sub(highlight_symbol_width);
constraints.push(Constraint::Length(highlight_symbol_width));
}
for (i, constraint) in self.widths.iter().enumerate() {
ccs.push(variables[i] | GE(WEAK) | 0.);
ccs.push(match *constraint {
Constraint::Length(v) => variables[i] | EQ(MEDIUM) | f64::from(v),
Constraint::Percentage(v) => {
variables[i] | EQ(WEAK) | (f64::from(v * available_width) / 100.0)
}
Constraint::Ratio(n, d) => {
variables[i]
| EQ(WEAK)
| (f64::from(available_width) * f64::from(n) / f64::from(d))
}
Constraint::Min(v) => variables[i] | GE(WEAK) | f64::from(v),
Constraint::Max(v) => variables[i] | LE(WEAK) | f64::from(v),
})
for constraint in self.widths {
constraints.push(*constraint);
constraints.push(Constraint::Length(self.column_spacing));
}
solver
.add_constraint(
variables
.iter()
.fold(Expression::from_constant(0.), |acc, v| acc + *v)
| LE(REQUIRED)
| f64::from(available_width),
)
.unwrap();
solver.add_constraints(&ccs).unwrap();
let mut widths = vec![0; variables.len()];
for &(var, value) in solver.fetch_changes() {
let index = var_indices[&var];
let value = if value.is_sign_negative() {
0
} else {
value.round() as u16
};
widths[index] = value;
if !self.widths.is_empty() {
constraints.pop();
}
// Cassowary could still return columns widths greater than the max width when there are
// fixed length constraints that cannot be satisfied. Therefore, we clamp the widths from
// left to right.
let mut available_width = max_width;
for w in &mut widths {
*w = available_width.min(*w);
available_width = available_width
.saturating_sub(*w)
.saturating_sub(self.column_spacing);
let mut chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.expand_to_fill(false)
.split(Rect {
x: 0,
y: 0,
width: max_width,
height: 1,
});
if has_selection {
chunks.remove(0);
}
widths
chunks.iter().step_by(2).map(|c| c.width).collect()
}
fn get_row_bounds(
@@ -334,6 +302,7 @@ impl<'a> Table<'a> {
offset: usize,
max_height: u16,
) -> (usize, usize) {
let offset = offset.min(self.rows.len().saturating_sub(1));
let mut start = offset;
let mut end = offset;
let mut height = 0;
@@ -366,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
@@ -414,9 +374,7 @@ impl<'a> StatefulWidget for Table<'a> {
let has_selection = state.selected.is_some();
let columns_widths = self.get_columns_widths(table_area.width, has_selection);
let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = iter::repeat(" ")
.take(highlight_symbol.width())
.collect::<String>();
let blank_symbol = " ".repeat(highlight_symbol.width());
let mut current_height = 0;
let mut rows_height = table_area.height;

View File

@@ -0,0 +1,315 @@
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use crate::widgets::{InteractiveWidgetState, InteractionOutcome, TextInputState};
impl InteractiveWidgetState for TextInputState {
fn handle_event(&mut self, event: Event) -> InteractionOutcome {
if !self.is_focused() {
return InteractionOutcome::Bubble;
}
match event {
Event::Key(key) => self.handle_key(key),
_ => InteractionOutcome::Bubble,
}
}
fn is_focused(&self) -> bool {
self.is_focused()
}
fn focus(&mut self) {
self.focus()
}
fn unfocus(&mut self) {
self.unfocus()
}
}
impl TextInputState {
// used in tests
#[allow(dead_code)]
fn up_to_cursor(&self) -> &str {
&self.value[0..self.cursor_pos as usize]
}
fn handle_key(&mut self, key: KeyEvent) -> InteractionOutcome {
if key.modifiers == KeyModifiers::ALT || key.modifiers == KeyModifiers::CONTROL {
self.handle_modifiers(key.modifiers, key.code)
} else {
self.handle_plain(key.code)
}
}
fn word_boundary_idx_under_cursor(&self, scan_backwards: bool) -> usize {
let value_as_chars = self.get_value().chars().collect::<Vec<_>>();
let mut char_pairs: Vec<(usize, &[char])> = value_as_chars
.windows(2) // work in doubles
.enumerate() // idx of the first char
.collect();
if scan_backwards {
char_pairs = char_pairs
.into_iter()
.take(self.cursor_pos.saturating_sub(1))
.rev()
.collect();
} else {
char_pairs = char_pairs.into_iter().skip(self.cursor_pos).collect()
}
if let Some((idx, _chars)) = char_pairs.iter().find(|(_, chars)| {
// find a boundary where we go from non-whitespace to whitespace
match (chars[0].is_whitespace(), chars[1].is_whitespace()) {
(true, true) => false,
(true, false) => scan_backwards,
(false, true) => !scan_backwards,
(false, false) => false,
}
}) {
// println!("bounry at {}: '{}{}'", idx, _chars[0], _chars[1]);
if scan_backwards {
idx + 1
} else {
idx + 2
}
} else {
// no whitespace boundary found, remove to start of string
if scan_backwards {
0
} else {
self.value.len()
}
}
}
fn handle_modifiers(&mut self, modifiers: KeyModifiers, code: KeyCode) -> InteractionOutcome {
match (modifiers, code) {
// delete to current word start
(KeyModifiers::CONTROL, KeyCode::Char('w')) => {
// find the first boundary going from non-whitespace to whitespace,
// going backwards from the cursor position
// println!("up to cursor ({}): '{}'", self.cursor_pos, self.up_to_cursor());
let remove_to = self.cursor_pos as usize;
let remove_from = self.word_boundary_idx_under_cursor(true);
// println!("removing span '{}'", &self.value.as_str()[remove_from..remove_to]);
// and collect everything that isn't between [remove_from..remove_to)
self.cursor_pos = remove_from;
self.value = self
.value
.chars()
.take(remove_from)
.chain(self.value.chars().skip(remove_to))
.collect();
}
// jump to end of line
(KeyModifiers::CONTROL, KeyCode::Char('e')) => {
self.cursor_pos = self.value.len();
}
// jump to start of line
(KeyModifiers::CONTROL, KeyCode::Char('a')) => {
self.cursor_pos = 0;
}
// jump back a word
(KeyModifiers::ALT, KeyCode::Char('b')) => {
self.cursor_pos = self.word_boundary_idx_under_cursor(true);
}
// jump forward a word
(KeyModifiers::ALT, KeyCode::Char('f')) => {
self.cursor_pos = self.word_boundary_idx_under_cursor(false);
}
_ => return InteractionOutcome::Bubble,
}
InteractionOutcome::Consumed
}
fn handle_plain(&mut self, code: KeyCode) -> InteractionOutcome {
match code {
KeyCode::Backspace => {
if self.cursor_pos > 0 {
self.cursor_pos -= 1;
self.value.remove(self.cursor_pos as usize);
}
}
KeyCode::Char(c) => {
self.value.insert(self.cursor_pos as usize, c);
self.cursor_pos += 1;
}
KeyCode::Left => {
if self.cursor_pos > 0 {
self.cursor_pos -= 1;
}
}
KeyCode::Right => {
if self.cursor_pos < self.value.len() {
self.cursor_pos += 1;
}
}
_ => return InteractionOutcome::Bubble,
};
InteractionOutcome::Consumed
}
}
#[cfg(test)]
mod test {
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use crate::widgets::{InteractiveWidgetState, InteractionOutcome, TextInputState};
macro_rules! assert_consumed {
($expr:expr) => {
assert_eq!(InteractionOutcome::Consumed, $expr)
};
}
#[test]
fn test_basics() {
let mut state = TextInputState::default();
// don't change when not focused
assert_eq!(InteractionOutcome::Bubble, state.handle_event(plain('a')));
assert_eq!("", state.get_value());
assert_eq!(0, state.cursor_pos);
state.focus();
assert_consumed!(state.handle_event(code(KeyCode::Left)));
assert_eq!(0, state.cursor_pos);
assert_consumed!(state.handle_event(code(KeyCode::Right)));
assert_eq!(0, state.cursor_pos);
assert_consumed!(state.handle_event(plain('a')));
assert_eq!("a", state.get_value());
assert_eq!(1, state.cursor_pos);
// build up a multi-char value
state.handle_event(plain('s'));
state.handle_event(plain('d'));
state.handle_event(plain('f'));
assert_eq!("asdf", state.get_value());
assert_eq!(4, state.cursor_pos);
// remove from end
state.handle_event(bksp());
assert_eq!("asd", state.get_value());
assert_eq!(3, state.cursor_pos);
// move cursor to middle
assert_eq!("asd", state.up_to_cursor());
state.handle_event(code(KeyCode::Left));
assert_eq!("as", state.up_to_cursor());
assert_eq!(2, state.cursor_pos);
assert_eq!("asd", state.get_value());
// remove from middle
state.handle_event(bksp());
assert_eq!(1, state.cursor_pos);
assert_eq!("ad", state.get_value());
}
#[test]
fn test_ctrl_w_works() {
let mut state = TextInputState::default();
state.focus();
// ctrl+w word removal, from the end of a word
state.set_value("foo bar baz smaz");
state.set_cursor(18);
assert_consumed!(state.handle_event(ctrl('w')));
assert_eq!("foo bar baz ", state.get_value());
assert_eq!(14, state.cursor_pos);
// remove runs of trailing whitespace + word
state.handle_event(ctrl('w'));
assert_eq!("foo bar ", state.get_value());
assert_eq!(8, state.cursor_pos);
// remove from middle of word
state.handle_event(code(KeyCode::Left));
state.handle_event(code(KeyCode::Left));
assert_eq!("foo ba", state.up_to_cursor());
state.handle_event(ctrl('w'));
assert_eq!("foo r ", state.get_value());
assert_eq!(4, state.cursor_pos);
// remove at start of word
state.handle_event(ctrl('w'));
assert_eq!("r ", state.get_value());
assert_eq!(0, state.cursor_pos);
// remove when buffer is empty
state.set_value("");
assert_eq!(0, state.cursor_pos);
assert_consumed!(state.handle_event(ctrl('w')));
}
#[test]
fn test_cursor_movement() {
let mut state = TextInputState::default();
state.focus();
state.set_value("foo bar baz");
state.set_cursor(0);
assert_consumed!(state.handle_event(ctrl('e')));
assert_eq!("foo bar baz", state.get_value());
assert_eq!(11, state.cursor_pos);
assert_consumed!(state.handle_event(ctrl('a')));
assert_eq!("foo bar baz", state.get_value());
assert_eq!(0, state.cursor_pos);
assert_consumed!(state.handle_event(alt('f')));
assert_eq!("foo bar baz", state.get_value());
assert_eq!(4, state.cursor_pos);
state.handle_event(alt('f'));
assert_eq!("foo bar baz", state.get_value());
assert_eq!(8, state.cursor_pos);
state.handle_event(alt('f'));
assert_eq!("foo bar baz", state.get_value());
assert_eq!(11, state.cursor_pos);
assert_consumed!(state.handle_event(alt('b')));
assert_eq!("foo bar baz", state.get_value());
assert_eq!(8, state.cursor_pos);
state.handle_event(alt('b'));
assert_eq!("foo bar baz", state.get_value());
assert_eq!(4, state.cursor_pos);
}
// helper macros + functions
fn ctrl(c: char) -> Event {
Event::Key(KeyEvent {
code: KeyCode::Char(c),
modifiers: KeyModifiers::CONTROL,
})
}
fn alt(c: char) -> Event {
Event::Key(KeyEvent {
code: KeyCode::Char(c),
modifiers: KeyModifiers::ALT,
})
}
fn plain(c: char) -> Event {
Event::Key(KeyEvent {
code: KeyCode::Char(c),
modifiers: KeyModifiers::NONE,
})
}
fn code(code: KeyCode) -> Event {
Event::Key(KeyEvent {
code,
modifiers: KeyModifiers::NONE,
})
}
fn bksp() -> Event {
code(KeyCode::Backspace)
}
}

View File

@@ -0,0 +1,204 @@
use std::borrow::Cow;
use crate::{
layout::Rect,
style::{Color, Modifier, Style},
text::{Span, Text},
widgets::Block,
};
use super::{InteractiveWidget, Paragraph};
#[cfg(feature = "crossterm")]
mod crossterm_interactive;
#[derive(Debug, Clone)]
pub struct TextInput<'a> {
// Block to draw the text input inside (convenience function) - default: None
optional_block: Option<Block<'a>>,
// Placeholder text - what's shown if the state value is "" - default: None
placeholder: Option<Text<'a>>,
// Render as a read-only input - that is, it will not be focused - default: false
is_read_only: bool,
// Style to render the widget when focused - default: Bold style
focused_style: Style,
// Style to apply to displayed text - overriden by focused_style when focused
text_style: Style,
}
impl<'a> TextInput<'a> {
pub fn new() -> TextInput<'a> {
Default::default()
}
pub fn block(mut self, block: Block<'a>) -> TextInput<'a> {
self.optional_block = Some(block);
self
}
pub fn read_only(mut self, read_only: bool) -> TextInput<'a> {
self.is_read_only = read_only;
self
}
pub fn placeholder_text<T>(mut self, placeholder_text: T) -> TextInput<'a>
where
T: Into<Cow<'a, str>>,
{
self.placeholder = Some(
Span::styled(
placeholder_text,
Style::default()
.fg(Color::Black)
.add_modifier(Modifier::ITALIC),
)
.into(),
);
self
}
pub fn placeholder(mut self, placeholder: Text<'a>) -> TextInput<'a> {
self.placeholder = Some(placeholder);
self
}
pub fn focused_style(mut self, style: Style) -> TextInput<'a> {
self.focused_style = style;
self
}
pub fn text_style(mut self, style: Style) -> TextInput<'a> {
self.text_style = style;
self
}
}
impl<'a> Default for TextInput<'a> {
fn default() -> Self {
Self {
optional_block: Default::default(),
placeholder: Default::default(),
is_read_only: false,
focused_style: Style::default().add_modifier(Modifier::BOLD),
text_style: Default::default(),
}
}
}
#[derive(Debug, Clone)]
pub struct TextInputState {
// Underlying value of the text input field
value: String,
// Position in the text input to insert / remove text from
cursor_pos: usize,
// Is the input focused?
is_focused: bool,
// Can the input take focus?
can_take_focus: bool,
}
impl TextInputState {
pub fn with_value(value: &str) -> TextInputState {
TextInputState {
value: value.to_string(),
cursor_pos: value.len(),
..Default::default()
}
}
pub fn can_take_focus(&mut self, can_take_focus: bool) {
self.can_take_focus = can_take_focus;
if !can_take_focus {
self.unfocus();
}
}
pub fn is_focused(&self) -> bool {
self.can_take_focus && self.is_focused
}
pub fn focus(&mut self) {
if self.can_take_focus {
self.is_focused = true;
}
}
pub fn unfocus(&mut self) {
self.is_focused = false;
}
pub fn set_value(&mut self, val: &str) {
self.value = val.to_string();
self.cursor_pos = std::cmp::min(self.cursor_pos, self.value.len());
}
pub fn set_cursor(&mut self, pos: usize) {
self.cursor_pos = pos;
}
pub fn get_value(&self) -> &String {
&self.value
}
}
impl Default for TextInputState {
fn default() -> Self {
Self {
value: Default::default(),
is_focused: false,
cursor_pos: 0,
can_take_focus: true,
}
}
}
impl<'a> InteractiveWidget for TextInput<'a> {
type State = TextInputState;
fn render<'b, B: crate::backend::Backend + 'b>(
mut self,
area: Rect,
frame: &mut crate::Frame<'b, B>,
state: &Self::State,
) {
let is_focused = !self.is_read_only && state.is_focused;
let area = if let Some(block) = self.optional_block.take() {
let block = if is_focused {
block.style(self.focused_style)
} else {
block
};
let inner = block.inner(area);
frame.render_widget(block, area);
inner
} else {
area
};
let contents = if state.get_value().is_empty() {
match self.placeholder {
Some(placeholder) => placeholder,
None => "".into(),
}
} else {
let value = state.get_value();
if is_focused {
Span::styled(value, self.focused_style).into()
} else {
Span::styled(value, self.text_style).into()
}
};
let paragraph = Paragraph::new(contents);
frame.render_widget(paragraph, area);
if is_focused {
frame.set_cursor(area.x + (state.cursor_pos as u16), area.y);
}
}
fn render_mut<'b, B: crate::backend::Backend + 'b>(
self,
area: Rect,
frame: &mut crate::Frame<'b, B>,
state: &mut Self::State,
) {
self.render(area, frame, state);
}
}

View File

@@ -1,7 +1,7 @@
use tui::{
backend::TestBackend,
buffer::Buffer,
layout::Rect,
layout::{Alignment, Rect},
style::{Color, Style},
text::Span,
widgets::{Block, Borders},
@@ -211,3 +211,136 @@ fn widgets_block_renders_on_small_areas() {
Buffer::with_lines(vec!["┌Test─"]),
);
}
#[test]
fn widgets_block_title_alignment() {
let test_case = |alignment, borders, expected| {
let backend = TestBackend::new(15, 2);
let mut terminal = Terminal::new(backend).unwrap();
let block = Block::default()
.title(Span::styled("Title", Style::default()))
.title_alignment(alignment)
.borders(borders);
let area = Rect {
x: 1,
y: 0,
width: 13,
height: 2,
};
terminal
.draw(|f| {
f.render_widget(block, area);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
// title top-left with all borders
test_case(
Alignment::Left,
Borders::ALL,
Buffer::with_lines(vec![" ┌Title──────┐ ", " └───────────┘ "]),
);
// title top-left without top border
test_case(
Alignment::Left,
Borders::LEFT | Borders::BOTTOM | Borders::RIGHT,
Buffer::with_lines(vec![" │Title │ ", " └───────────┘ "]),
);
// title top-left with no left border
test_case(
Alignment::Left,
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
Buffer::with_lines(vec![" Title───────┐ ", " ────────────┘ "]),
);
// title top-left without right border
test_case(
Alignment::Left,
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
Buffer::with_lines(vec![" ┌Title─────── ", " └──────────── "]),
);
// title top-left without borders
test_case(
Alignment::Left,
Borders::NONE,
Buffer::with_lines(vec![" Title ", " "]),
);
// title center with all borders
test_case(
Alignment::Center,
Borders::ALL,
Buffer::with_lines(vec![" ┌───Title───┐ ", " └───────────┘ "]),
);
// title center without top border
test_case(
Alignment::Center,
Borders::LEFT | Borders::BOTTOM | Borders::RIGHT,
Buffer::with_lines(vec![" │ Title │ ", " └───────────┘ "]),
);
// title center with no left border
test_case(
Alignment::Center,
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
Buffer::with_lines(vec![" ────Title───┐ ", " ────────────┘ "]),
);
// title center without right border
test_case(
Alignment::Center,
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
Buffer::with_lines(vec![" ┌───Title──── ", " └──────────── "]),
);
// title center without borders
test_case(
Alignment::Center,
Borders::NONE,
Buffer::with_lines(vec![" Title ", " "]),
);
// title top-right with all borders
test_case(
Alignment::Right,
Borders::ALL,
Buffer::with_lines(vec![" ┌──────Title┐ ", " └───────────┘ "]),
);
// title top-right without top border
test_case(
Alignment::Right,
Borders::LEFT | Borders::BOTTOM | Borders::RIGHT,
Buffer::with_lines(vec![" │ Title│ ", " └───────────┘ "]),
);
// title top-right with no left border
test_case(
Alignment::Right,
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
Buffer::with_lines(vec![" ───────Title┐ ", " ────────────┘ "]),
);
// title top-right without right border
test_case(
Alignment::Right,
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
Buffer::with_lines(vec![" ┌───────Title ", " └──────────── "]),
);
// title top-right without borders
test_case(
Alignment::Right,
Borders::NONE,
Buffer::with_lines(vec![" Title ", " "]),
);
}

42
tests/widgets_canvas.rs Normal file
View 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)
}

View File

@@ -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| {
@@ -47,6 +64,197 @@ fn widgets_chart_can_render_on_small_areas() {
test_case(2, 2);
}
#[test]
fn widgets_chart_handles_long_labels() {
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![
" ",
" ",
" ",
" ───────",
"AAA B",
],
);
test_case(
Some(("A", "BBBB")),
None,
Alignment::Left,
vec![
" ",
" ",
" ",
" ─────────",
"A BBBB",
],
);
test_case(
Some(("AAAAAAAAAAA", "B")),
None,
Alignment::Left,
vec![
" ",
" ",
" ",
" ───────",
"AAA B",
],
);
test_case(
Some(("A", "B")),
Some(("CCCCCCC", "D")),
Alignment::Left,
vec![
"D │ ",
"",
"CCC│ ",
" └──────",
" 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]
fn widgets_chart_can_have_axis_with_zero_length_bounds() {
let backend = TestBackend::new(100, 100);
@@ -257,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│",
"└──────────────────────────────────────────────────────────┘",
]);

View File

@@ -2,8 +2,9 @@ use tui::{
backend::TestBackend,
buffer::Buffer,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style},
style::{Color, Modifier, Style},
symbols,
text::Span,
widgets::{Block, Borders, Gauge, LineGauge},
Terminal,
};
@@ -116,6 +117,89 @@ fn widgets_gauge_renders_no_unicode() {
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_gauge_applies_styles() {
let backend = TestBackend::new(12, 5);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let gauge = Gauge::default()
.block(
Block::default()
.title(Span::styled("Test", Style::default().fg(Color::Red)))
.borders(Borders::ALL),
)
.gauge_style(Style::default().fg(Color::Blue).bg(Color::Red))
.percent(43)
.label(Span::styled(
"43%",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
));
f.render_widget(gauge, f.size());
})
.unwrap();
let mut expected = Buffer::with_lines(vec![
"┌Test──────┐",
"│ │",
"│ 43% │",
"│ │",
"└──────────┘",
]);
// title
expected.set_style(Rect::new(1, 0, 4, 1), Style::default().fg(Color::Red));
// gauge area
expected.set_style(
Rect::new(1, 1, 10, 3),
Style::default().fg(Color::Blue).bg(Color::Red),
);
// filled area
for y in 1..4 {
expected.set_style(
Rect::new(1, y, 4, 1),
// filled style is invert of gauge_style
Style::default().fg(Color::Red).bg(Color::Blue),
);
}
// label (foreground and modifier from label style)
expected.set_style(
Rect::new(4, 2, 1, 1),
Style::default()
.fg(Color::Green)
// "4" is in the filled area so background is gauge_style foreground
.bg(Color::Blue)
.add_modifier(Modifier::BOLD),
);
expected.set_style(
Rect::new(5, 2, 2, 1),
Style::default()
.fg(Color::Green)
// "3%" is not in the filled area so background is gauge_style background
.bg(Color::Red)
.add_modifier(Modifier::BOLD),
);
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_gauge_supports_large_labels() {
let backend = TestBackend::new(10, 1);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let gauge = Gauge::default()
.percent(43)
.label("43333333333333333333333333333%");
f.render_widget(gauge, f.size());
})
.unwrap();
let expected = Buffer::with_lines(vec!["4333333333"]);
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_line_gauge_renders() {
let backend = TestBackend::new(20, 4);

View File

@@ -4,6 +4,7 @@ use tui::{
layout::Rect,
style::{Color, Style},
symbols,
text::Spans,
widgets::{Block, Borders, List, ListItem, ListState},
Terminal,
};
@@ -86,3 +87,114 @@ fn widgets_list_should_truncate_items() {
terminal.backend().assert_buffer(&case.expected);
}
}
#[test]
fn widgets_list_should_clamp_offset_if_items_are_removed() {
let backend = TestBackend::new(10, 4);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = ListState::default();
// render with 6 items => offset will be at 2
state.select(Some(5));
terminal
.draw(|f| {
let size = f.size();
let items = vec![
ListItem::new("Item 0"),
ListItem::new("Item 1"),
ListItem::new("Item 2"),
ListItem::new("Item 3"),
ListItem::new("Item 4"),
ListItem::new("Item 5"),
];
let list = List::new(items).highlight_symbol(">> ");
f.render_stateful_widget(list, size, &mut state);
})
.unwrap();
let expected = Buffer::with_lines(vec![" Item 2 ", " Item 3 ", " Item 4 ", ">> Item 5 "]);
terminal.backend().assert_buffer(&expected);
// render again with 1 items => check offset is clamped to 1
state.select(Some(1));
terminal
.draw(|f| {
let size = f.size();
let items = vec![ListItem::new("Item 3")];
let list = List::new(items).highlight_symbol(">> ");
f.render_stateful_widget(list, size, &mut state);
})
.unwrap();
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);
}

View File

@@ -354,12 +354,12 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Hea Head2 Hea",
"│Hea Head2 He ",
"│ │",
"│Row Row12 Row",
"│Row Row22 Row",
"│Row Row32 Row",
"│Row Row42 Row",
"│Row Row12 Ro ",
"│Row Row22 Ro ",
"│Row Row32 Ro ",
"│Row Row42 Ro ",
"│ │",
"│ │",
"└────────────────────────────┘",
@@ -715,3 +715,109 @@ fn widgets_table_should_render_even_if_empty() {
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_table_columns_dont_panic() {
let test_case = |state: &mut TableState, table: Table, width: u16| {
let backend = TestBackend::new(width, 8);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
f.render_stateful_widget(table, size, state);
})
.unwrap();
};
// based on https://github.com/fdehau/tui-rs/issues/470#issuecomment-852562848
let table1_width = 98;
let table1 = Table::new(vec![Row::new(vec!["r1", "r2", "r3", "r4"])])
.header(Row::new(vec!["h1", "h2", "h3", "h4"]))
.block(Block::default().borders(Borders::ALL))
.highlight_symbol(">> ")
.column_spacing(1)
.widths(&[
Constraint::Percentage(15),
Constraint::Percentage(15),
Constraint::Percentage(25),
Constraint::Percentage(45),
]);
let mut state = TableState::default();
// select first, which would cause a panic before fix
state.select(Some(0));
test_case(&mut state, table1.clone(), table1_width);
}
#[test]
fn widgets_table_should_clamp_offset_if_rows_are_removed() {
let backend = TestBackend::new(30, 8);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = TableState::default();
// render with 6 items => offset will be at 2
state.select(Some(5));
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![
Row::new(vec!["Row01", "Row02", "Row03"]),
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
Row::new(vec!["Row51", "Row52", "Row53"]),
])
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.widths(&[
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
])
.column_spacing(1);
f.render_stateful_widget(table, size, &mut state);
})
.unwrap();
let expected = Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row21 Row22 Row23 │",
"│Row31 Row32 Row33 │",
"│Row41 Row42 Row43 │",
"│Row51 Row52 Row53 │",
"└────────────────────────────┘",
]);
terminal.backend().assert_buffer(&expected);
// render with 1 item => offset will be at 1
state.select(Some(1));
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![Row::new(vec!["Row31", "Row32", "Row33"])])
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.widths(&[
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
])
.column_spacing(1);
f.render_stateful_widget(table, size, &mut state);
})
.unwrap();
let expected = Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row31 Row32 Row33 │",
"│ │",
"│ │",
"│ │",
"└────────────────────────────┘",
]);
terminal.backend().assert_buffer(&expected);
}