Compare commits

...

188 Commits

Author SHA1 Message Date
Caleb Bassi
4f5594beff feat(sparkline): Add show baseline option
Adds an option to the sparkline widget to render the minimum value as
a baseline symbol rather than as an empty cell.
2023-05-05 14:03:41 -07:00
Josh McKinney
9da02078fe test(buffer): add assert_buffer_eq and impl Debug
- The implementation of Debug is customized to make it easy to use the
output (particularly the content) directly when writing tests (by
surrounding it with `Buffer::with_lines(vec![])`). The styles part of
the message shows the position of every style change, rather than the
style of each cell, which reduces the verbosity of the detail, while
still showing everything necessary to debug the buffer.

```rust
Buffer {
    area: Rect { x: 0, y: 0, width: 12, height: 2 },
    content: [
        "Hello World!",
        "G'day World!",
    ],
    styles: [
        x: 0, y: 0, fg: Reset, bg: Reset, modifier: (empty),
        x: 0, y: 1, fg: Green, bg: Yellow, modifier: BOLD,
    ]
}
```

- The assert_buffer_eq! macro shows debug view and diff of the two
buffers, which makes it easy to understand exactly where the difference
is.

- Also adds a unit test for buffer_set_string_multi_width_overwrite
which was missing from the buffer tests
2023-05-05 12:07:28 -07:00
Josh McKinney
cf1a759fa5 test(widget): add unit tests for Paragraph (#156) 2023-05-04 21:29:40 +02:00
a-kenji
86c3fc9fac feat(test): expose test buffer (#160)
Allow a way to expose the buffer of the `TestBackend`,
to easier support different testing methodologies.
2023-05-04 21:28:55 +02:00
Josh McKinney
5f12f06297 ci: add ci, build, and revert to allowed commit types
This is the same list as https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional\#rules
2023-05-04 04:12:55 -07:00
Lowell Thoerner
33b3f4e312 feat(widget): add style methods to Span, Spans, Text (#148)
- add patch_style() and reset_style() to `Span` and `Spans`
- add reset_style() to `Text`
- updated doc for patch_style() on `Text` for clarity.
2023-05-02 15:30:05 -07:00
Josh McKinney
603ec3f10a docs(block): add example for block.inner (#158) 2023-05-02 06:48:38 +01:00
Leon Sautour
37bb82e87d docs(readme): add acknowledgement section (#154) 2023-04-27 14:10:26 +02:00
Orhun Parmaksız
047e0b7e8d style(readme): update project introduction in README.md (#153) 2023-04-27 12:03:37 +01:00
Orhun Parmaksız
782820c34a feat(widget): support adding padding to Block (#20)
Co-authored-by: Igor Mandello <igormandello@gmail.com>
Co-authored-by: Léon Sautour <leon@sautour.net>
2023-04-27 12:30:13 +02:00
Leon Sautour
26cf1f7a89 refactor(examples): refactor paragraph example (#152) 2023-04-27 11:06:45 +01:00
Erich Heine
f7ab4f04ac feat(calendar): add calendar widget (#138) 2023-04-26 23:02:35 +02:00
Erich Heine
2751f08bdc fix(142): cleanup doc example (#145) 2023-04-21 16:06:14 +02:00
wcampbell
0904d448e0 docs(apps): ix rsadsb/adsb_deku radar link (#140) 2023-04-18 18:50:34 +02:00
BADR
fab7952af6 docs(apps): add tenere (#141) 2023-04-18 18:48:53 +02:00
Conrad Ludgate
6af75d6d40 feat(terminal)!: add inline viewport (#114)
Co-authored-by: Florian Dehau <work@fdehau.com>
Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2023-04-17 14:23:50 +02:00
Josh McKinney
5f1a37f0db fix(canvas)!: use full block for Marker::Block (#133) 2023-04-15 17:40:28 +02:00
Josh McKinney
b7bd3051b1 feat(sparkline): finish #1 Sparkline directions PR (#134)
Co-authored-by: Joseph Price <pricejosephd@gmail.com>
2023-04-14 19:15:56 +02:00
Orhun Parmaksız
7adc3fe19b feat(block): support placing the title on bottom (#36)
Co-authored-by: Spencer Gouw <swyverng55@g.ucla.edu>
Co-authored-by: Benjscho <47790940+Benjscho@users.noreply.github.com>
Co-authored-by: Arijit Basu <sayanarijit@gmail.com>
2023-04-13 22:24:31 +02:00
lesleyrs
4842885aa6 fix(examples): update input in examples to only use press events (#129) 2023-04-13 22:21:35 +02:00
Xithrius
00e8c0d1b0 docs(apps): add twitch-tui (#124) 2023-04-11 13:05:47 +02:00
MeowKing
239efa5fbd docs(readme): update project description (#127) 2023-04-07 16:28:42 +02:00
Brook Jeynes [SSW]
a9ba23bae8 docs(apps): add oxycards (#113) 2023-03-27 10:43:40 +02:00
Orhun Parmaksız
62930f2821 refactor(example): remove redundant vec![] in user_input example (#26)
Co-authored-by: rhysd <lin90162@yahoo.co.jp>
2023-03-25 14:45:28 +01:00
FujiApple
4334c71bc7 docs(apps): re-add trippy to APPS.md (#117) 2023-03-25 12:29:52 +01:00
kpcyrd
21a029f17e refactor(style): Mark some Style fns const so they can be defined globally (#115) 2023-03-24 17:34:06 +01:00
Orhun Parmaksız
9d37c3bd45 docs(changelog): update the empty profile link in contributors (#112) 2023-03-23 09:40:39 +05:30
Arijit Basu
343ec220b4 chore(release): prepare for 0.20.1 (#110) 2023-03-22 23:57:54 +05:30
Josh McKinney
2da4c10384 docs: fixup remaining tui references (#106) 2023-03-22 11:03:45 +05:30
acheronfail
829dfee9e5 Update APPS.md (#108) 2023-03-22 10:57:20 +05:30
UncleScientist
26c7bcfdd5 fix(style): bold needs a bit (#104) 2023-03-20 23:09:27 +05:30
todoesverso
cf1b05d16e docs(apps): add "logss" to apps (#105) 2023-03-20 18:20:22 +01:00
Orhun Parmaksız
cfa8b8042a chore(release): prepare for 0.20.0 (#97) 2023-03-19 18:11:15 +01:00
Orhun Parmaksız
f0c0985708 docs(readme): update crate status badge (#102) 2023-03-19 17:55:48 +01:00
Leon Sautour
73c937bc30 docs(readme): small edits before first release (#101) 2023-03-19 17:49:26 +01:00
Orhun Parmaksız
33e67abbe9 docs(canvas): add documentation for x_bounds, y_bounds (#35)
Co-authored-by: girvel <widauka@ya.ru>
Co-authored-by: Leon Sautour <leon1.sautour@epitech.eu>
Co-authored-by: Arijit Basu <sayanarijit@users.noreply.github.com>
2023-03-19 17:30:23 +01:00
Arijit Basu
c20fb723ef docs(readme): add install instruction and update title (#100) 2023-03-19 16:08:04 +01:00
Orhun Parmaksız
ccd142df97 docs(apps): move 'apps using ratatui' to dedicated file (#98) (#99) 2023-03-19 01:30:47 +01:00
Orhun Parmaksız
f6dbd1c0b5 docs(readme): add systeroid to application list (#92) 2023-03-18 08:11:44 +05:30
Orhun Parmaksız
d38d185d1c feat(cd): add continuous deployment workflow (#93) 2023-03-17 21:19:01 +01:00
Qichao Lan
ef79b72471 docs(readme): add glicol-cli to showcase list (#95) 2023-03-17 21:18:04 +01:00
Orhun Parmaksız
ed12ab16e0 chore(cargo): update project metadata (#94) 2023-03-17 17:03:49 +01:00
Orhun Parmaksız
24820cfcff chore(ci): integrate typos for checking typos (#91) 2023-03-16 15:00:52 +01:00
Kian-Meng Ang
e10f62663e docs: fix typos (#90) 2023-03-16 18:56:50 +05:30
Conrad Ludgate
02573b0ad2 better safe shared layout cache (#62) 2023-03-12 18:23:30 +05:30
Orhun Parmaksız
0dc39434c2 Fix: cassowary/layouts: add extra constraints for fixing Min(v)/Max(v) combination. (#31)
Co-authored-by: Simon Allen <simon@simonallen.org>
2023-03-11 17:11:13 +05:30
Linda_pp
33acfce083 fix(ci): Test MSRV compatibility on CI (#85) 2023-03-08 22:07:19 +05:30
Arijit Basu
79d0eadbd6 docs: update to build more backends (#81)
Co-authored-by: Doug Goldstein <cardoe@cardoe.com>
2023-03-05 13:25:36 +01:00
Orhun Parmaksız
73f7f16298 fix(ci): Bump Rust version to 1.63.0 (#80) 2023-03-02 07:41:06 +01:00
Orhun Parmaksız
66eb0e42fe chore(ci): Change the target branch to main (#79)
Co-authored-by: Leon Sautour <leon@sautour.net>
2023-03-01 11:06:14 +01:00
Orhun Parmaksız
052ae53b6e chore: Integrate committed for checking conventional commits (#77)
Closes #50
2023-02-25 14:36:49 +01:00
Orhun Parmaksız
1c0ed3268b fix(ci): use env for the cargo-make version (#76) 2023-02-22 13:40:04 +01:00
Orhun Parmaksız
0456abb327 fix(widget)!: List should not ignore empty string items (#42)
Fixes issue #680. Handles the case where a list item is created with an empty string, which is not split by the lines iterator.

Co-authored-by: Collin O'Connor <collinoconnor2@gmail.com>
Co-authored-by: Arijit Basu <sayanarijit@users.noreply.github.com>
Co-authored-by: Arijit Basu <sayanarijit@gmail.com>
2023-02-21 13:22:37 +01:00
Jack Wills
ec50458491 docs(readme): Add oxker to application list (#74) 2023-02-21 11:33:52 +01:00
牧心
b834ccaa2f docs(readme): Add app kubectl-watch which uses tui (#73) 2023-02-20 15:35:37 +01:00
Leon Sautour
ffb3de6c36 docs(github): remove pull request template (#68) 2023-02-20 14:32:38 +01:00
Leon Sautour
454c8459f2 docs(contributing): specify the use of unsafe for optimization (#67) 2023-02-20 14:28:49 +01:00
CK Aznable
94a0d09591 docs(readme): add poketex to 'apps using tui' in README (#64) 2023-02-19 12:42:09 +01:00
Orhun Parmaksız
9b7a6ed85d feat(widget): add offset() to TableState (#10)
Co-authored-by: Aaron Rennow <arennow@outlook.com>
2023-02-18 03:11:34 +01:00
Orhun Parmaksız
7e31035114 refactor(style): make bitflags smaller (#13)
Co-authored-by: Cédric Barreteau <cbarrete@users.noreply.github.com>
2023-02-18 03:10:40 +01:00
Orhun Parmaksız
e15a6146f8 feat(widget): add width() to ListItem (#17)
Co-authored-by: Nicholas Howard <nicholas.howard@novatechautomation.com>
2023-02-18 03:09:32 +01:00
Linda_pp
e49cb1126b fix(doc): add 3rd party libraries accidentally removed at #21 (#61) 2023-02-17 13:01:16 +01:00
Linda_pp
142bc5720e feat(ci): add MacOS to CI (#60) 2023-02-17 12:59:18 +01:00
Linda_pp
feaeb7870f fix(ci): fix deprecation warnings on CI (#58)
* fix(ci): fix deprecation warnings on CI

* fix(ci): remove unnecessary step in CI workflow
2023-02-17 06:25:51 +05:30
Linda_pp
9df0eefe49 chore(ci): re-enable clippy on CI (#59) 2023-02-16 18:51:54 +05:30
Linda_pp
8e89a9377a chore: update rust-version to 1.59 in Cargo.toml (#57) 2023-02-16 08:38:30 +05:30
Orhun Parmaksız
d3df8fe7ef fix: Fix user_input example double key press registered on windows
Co-authored-by: 朕与将军解战袍 <72246322+a1393323447@users.noreply.github.com>
2023-02-15 22:52:08 +01:00
Arijit Basu
9feda988a5 chore: Update deps (#51)
* Update deps

Also, temporarily disabled clippy check. Can be discussed in #49.

* Fix termion demo

* chore: fix all clippy warnings

* Call into_raw_mode()

* Update min supported rust version

---------

Co-authored-by: rhysd <lin90162@yahoo.co.jp>
2023-02-15 18:29:50 +05:30
Orhun Parmaksız
9534d533e3 fix: Ignore zero-width symbol on rendering Paragraph
This fixes out-of-bounds crash on rendering `Paragraph` when zero-width
character is at end of line.

fix #642

Co-authored-by: rhysd <lin90162@yahoo.co.jp>
2023-02-15 13:56:54 +01:00
Orhun Parmaksız
85eefe1d8b docs: Expand "Apps" and "Third-party" sections (#21)
* README.md/#Apps using tui: Sort lexically

* README.md/#Apps using tui: Remove Hoard.

Closes #569.

* README.md/#Apps using tui: Add a description to each app.

* README.md/#Apps using tui: Add some more apps.

This is a curated addition.

Here are the apps I chose not to add:

```md
* [Chatui](https://github.com/xaerru/chatui) — ChatApp made using the standard library net module and Tui-rs
* [Corona-rs](https://github.com/varjolintu/corona-rs) — Corona virus statistics with Tui-rs
* [HTTP Request Tool](https://github.com/Callum-Irving/http-request-tool) — HTTP request sending tool similar to Insomnia, but uses a text user interface (TUI)
* [KRTirtho/portfolio](https://github.com/KRTirtho/portfolio) — A TUI based personal portfolio created using Rust & Tui-rs
* [Picterm](https://github.com/ksk001100/picterm) — TUI image viewer
```

* README.md/#Third party: add more projects

* Undo the capitalization

---------

Co-authored-by: Nicolas Girard <girard.nicolas@gmail.com>
Co-authored-by: Arijit Basu <sayanarijit@gmail.com>
2023-02-15 13:55:44 +01:00
Orhun Parmaksız
3343270680 docs: add tui-input and update xplr in README.md
Also update xplr description.

Co-authored-by: Arijit Basu <sayanarijit@gmail.com>
2023-02-14 19:13:21 +01:00
Orhun Parmaksız
bf9d502742 Update README.md (#39)
Co-authored-by: 晧暐 <henryliking@gmail.com>
2023-02-14 19:09:25 +01:00
Orhun Parmaksız
85c0779ac0 Update README.md (#40)
Co-authored-by: Tin Chung <56880684+chungquantin@users.noreply.github.com>
Co-authored-by: Arijit Basu <sayanarijit@users.noreply.github.com>
2023-02-14 21:29:11 +05:30
Orhun Parmaksız
070de44069 docs: Add hncli to list of applications made with tui-rs (#41)
Co-authored-by: pierreyoda <pierreyoda@users.noreply.github.com>
2023-02-14 16:54:14 +01:00
Orhun Parmaksız
33087e3a99 Fix typos (#45)
Co-authored-by: Howard Halim <howardhalim@gmail.com>
2023-02-14 10:26:07 +01:00
UncleScientist
fe4eb5e771 fix typos (#47) 2023-02-14 10:23:14 +01:00
Owletti
2fead23556 Clarify README.md fork status update
Clarify README.md
2023-02-14 10:22:07 +01:00
Leon Sautour
bc5a9e4c06 doc: Updated readme and contributing guide with updates about the fork (#46)
* doc: Updated readme and contributing guide with updates about the fork

* doc: added missing discord invite link
2023-02-13 16:10:08 +01:00
davidhelbig
fafad6c961 chore: fix typo in layout.rs (#619) 2022-08-14 15:58:02 +02:00
Florian Dehau
a4de409235 chore: add apps using tui 2022-08-14 15:51:31 +02:00
Florian Dehau
a05fd45959 Release v0.19.0 2022-08-14 15:38:31 +02:00
Florian Dehau
24de2f8a96 chore: bump crossterm to v0.25 2022-08-14 15:19:48 +02:00
Florian Dehau
eee37011a5 chore: fix clippy warnings 2022-08-14 14:59:04 +02:00
Florian Dehau
a67706bea0 chore(ci): bump cargo-make to v0.35.16 2022-08-14 14:43:38 +02:00
Linda_pp
faa69b6cfe chore: explicitly set MSRV to 1.56.1 in Cargo.toml 2022-08-14 14:24:54 +02:00
Florian Dehau
ba5ea2deff chore: update README 2022-08-14 13:58:53 +02: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
Florian Dehau
90a6a8f2d6 Release v0.15.0 2021-05-02 19:03:01 +02:00
Florian Dehau
414386e797 chore: update rand to 0.8 (#472) 2021-05-02 18:51:59 +02:00
Joey Ezechiëls
3a843d5074 fix(test): remove compile warning in TestBackend::assert_buffer (#466) 2021-05-02 18:27:47 +02:00
Luc Perkins
4e76bfa2ca chore: add Vector to list of apps using tui (#452)
Signed-off-by: Luc Perkins <luc@timber.io>
2021-02-21 14:58:57 +01:00
Simas Toleikis
8832281dcf Update crossterm to 0.19. 2021-01-04 23:26:04 +01:00
Florian Dehau
853f3d9200 feat(terminal): add a read-only view of the terminal state after the draw call 2021-01-04 22:18:28 +01:00
Florian Dehau
67e996c5f4 feat(examples): add third tab to demo to show colors 2021-01-01 15:41:49 +01:00
Florian Dehau
f09863faa0 Release v0.14.0 2021-01-01 14:51:08 +01:00
Florian Dehau
eb1e3be722 fix(widgets/block): make Block::inner return more accurate results on small areas 2020-12-13 17:21:10 +01:00
Sagie Gur-Ari
4ec902b96f chore: make run-examples available on all platforms (#429)
* Make examples available for all platforms
* limit windows to crossterm_demo only and make q exit demos work
2020-12-13 15:29:31 +01:00
Vadim Chekan
74243394d9 fix(widgets/table): draw table header and border even if rows are empty (#426) 2020-12-08 21:49:16 +01:00
Florian Dehau
e7f263efa7 chore(ci): fix cargo-make cache on windows runner 2020-12-07 23:58:27 +01:00
Florian Dehau
0991145c58 chore(ci): simplify ci workflow (#428)
* chore(ci): simplify ci workflow

* use more up to date action
* restrict actions allowed to run
* cache cargo-make
2020-12-06 18:31:54 +01:00
Florian Dehau
01d2a8588a chore(ci): reduce the number of triggered jobs 2020-12-06 16:58:07 +01:00
Florian Dehau
45431a2649 chore: add first contributing guidelines 2020-12-06 16:58:07 +01:00
Florian Dehau
0b78fb9201 chore: use cargo-make in the CI as well 2020-12-06 16:58:07 +01:00
Florian Dehau
9cdff275cb chore: replace make with cargo-make
`cargo-make` make it easier to provide developers of all platforms an unified build workflow.
2020-12-06 16:58:07 +01:00
Arne Beer
77c6e106e4 doc(examples): Add comments to "list" example and fix list direction (#425)
* Add docs to list example and fix list direction

* List example: review adjustments and typo fixes
2020-12-06 16:33:31 +01:00
Florian Dehau
efdd6bfb19 feat(tests): add tests covering new table features 2020-11-29 23:47:58 +01:00
Florian Dehau
117098d2d2 refactor(examples): add missing margin at the bottom of the header of table in the demo 2020-11-29 23:47:58 +01:00
Florian Dehau
f933d892aa chore: update CHANGELOG 2020-11-29 23:47:58 +01:00
Florian Dehau
5ea54792c0 refactor(widgets/table): more flexible table
- control over the style of each cell and its content using the styling capabilities of Text.
- rows with multiple lines.
- fix panics on small areas.
- less generic type parameters.
2020-11-29 23:47:58 +01:00
Tom Forbes
23a9280db7 chore: add gping to the lists of apps using tui (#422)
* Add gping to the lists of apps using tui
2020-11-29 19:28:53 +01:00
Florian Dehau
79e27b1778 refactor(widgets/gauge): stop using unicode blocks by default 2020-11-29 19:27:34 +01:00
DashEightMate
0a05579a1c feat(widgets/gauge): allow gauge to use unicode block for more descriptive progress (#377)
* gauge now uses unicode blocks for more descriptive progress

* removed unnecessary if

* changed function name to better reflect unicode

* standardized block symbols, added no unicode option, added tests

* formatting

* improved readability

* gauge tests now check color

* formatted
2020-11-29 19:20:08 +01:00
Tom Forbes
0030eb4a13 fix(tests): remove clippy warnings about single char push (#424) 2020-11-29 18:35:52 +01:00
pm100
5bf40343eb fix(widgets/paragraph): handle trailing nbsp in wrapped text (#405) 2020-11-15 20:03:33 +01:00
Florian Dehau
1e35f983c4 doc(style): improve documentation of Style 2020-11-14 21:29:56 +01:00
Florian Dehau
a15ac8870b feat(style): add a method to create a style that reset all properties until that point 2020-11-14 21:29:56 +01:00
Florian Dehau
8a27036a54 fix(widgets/block): allow Block to render on small areas 2020-11-14 20:32:10 +01:00
Florian Dehau
8543523f18 Release v0.13.0 2020-11-14 17:37:21 +01:00
acheronfail
5a9b59866b feat(widgets/listitem): derive PartialEq 2020-11-14 16:51:19 +01:00
Dheepak Krishnamurthy
dc76956215 chore: add taskwarrior-tui to the list of apps using tui-rs (#403) 2020-11-14 16:48:43 +01:00
Kemyt
98fb5e4bbd fix(widgets/table): take borders into account when percentage and ration constraints are used (#385)
* Fix percentage and ratio constraints for table to take borders into account

Percentage and ratio constraints don't take borders into account, which causes
the table to not be correctly aligned. This is easily seen when using 50/50
percentage split with bordered table. However fixing this causes the last column
of table to not get printed, so there is probably another problem with columns
width determination.

* Fix rounding of cassowary solved widths to eliminate imprecisions

* Fix formatting to fit convention

Co-authored-by: Kemyt <kemyt4@gmail.com>
2020-11-14 16:45:36 +01:00
Sebastian Thiel
25ff2e5e61 upgrade crossterm to v0.18
It reduces the amount of dependencies, among other improvements.
2020-09-29 23:28:45 +02:00
Florian Dehau
5050f1ce1c feat(widgets/gauge): add LineGauge variant of Gauge 2020-09-28 00:40:19 +02:00
Florian Dehau
51b691e7ac Release v0.12.0 2020-09-27 19:55:45 +02:00
Florian Dehau
c4cd0a5f31 fix(widgets/chart): use the correct style to draw legend and axis titles
Before this change, the style of the points drawn in the graph area could reused to draw the
title of the axis and the legend. Now the style of these components put on top of the graph area
is solely based on the widget style.
2020-09-27 19:12:35 +02:00
Florian Dehau
41142732ec feat(buffer): add a method to build a Style out of an existing Cell 2020-09-27 19:12:35 +02:00
Kemyt
62495c3bd1 fix(widgets/barchart): fix chart filled more than actual (#383)
* Fix barchart incorrectly showing chart filled more than actual

Determination of how filled the bar should be was incorrectly taking the
entire chart height into account, when in fact it should take height-1, because
of extra line with label. Because of that the chart appeared fuller than it
should and was full before reaching maximum value.

* Add a test case for checking if barchart fills correctly up to max value

Co-authored-by: Kemyt <kemyt4@gmail.com>
2020-09-27 17:15:44 +02:00
Brooks Rady
d00184a7c3 feat(text): extend Text to be stylable and extendable (#361)
* Extend `Text` to be extendable
* Add some documentation
2020-09-27 14:20:14 +02:00
Brooks Rady
ce32d5537d chore: clippy fixes (#386) 2020-09-27 13:41:28 +02:00
Luis Enrique Muñoz Martín
25921fa91a chore: added termchat to "apps using tui" (#371) 2020-09-20 18:23:10 +02:00
luak
932a496c3c chore: add rkm to the list of apps using tui (#376) 2020-09-20 16:43:02 +02:00
Florian Dehau
57862eeda6 Release v0.11.0 2020-09-20 16:34:44 +02:00
Florian Dehau
11df94d601 fix(examples): avoid panic when computing event poll timeout in crossterm demo (#380) 2020-09-20 15:51:44 +02:00
Brooks Rady
0abaa20de9 refactor: clean up some folds (#362) 2020-09-20 15:02:06 +02:00
Amjad Alsharafi
c35a1dd79f feat(widgets/canvas): added type Block in canvas markers (#350)
This allows for clearer colors than using Dot, especially when
decreasing the size of the terminal font in order to increase the
resolution of the canvas
2020-09-20 14:57:35 +02:00
alvinhochun
e0b2572eba refactor(backend/crossterm): support more style modifiers on Windows and fix build with Crossterm 0.17.8 (#368)
* Support more style modifiers on Windows
* Change Crossterm backend to write directly to buffer instead of String

Crossterm might actually do WinAPI calls instead of writing ANSI excape
codes so writing to an intermediate String may cause issues on older
versions of Windows. It also fails to compile with Crossterm 0.17.8 due
to Crossterm now expecting the writer to support `flush`, which String
doesn't.

Fixes #373
2020-09-20 14:54:16 +02:00
Cory Forsstrom
aada695b3f chore: add tickrs to apps using tui (#351) 2020-08-23 21:20:30 +02:00
Florian Dehau
90f3858eff feat(backend): keep the internal buffers in sync when the terminal is cleared. 2020-08-23 21:13:12 +02:00
84 changed files with 9221 additions and 3545 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 `ratatui` 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.
-->

19
.github/workflows/cd.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Continuous Deployment
on:
push:
tags:
- "v*.*.*"
jobs:
publish:
name: Publish on crates.io
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v3
- name: Publish
uses: actions-rs/cargo@v1
with:
command: publish
args: --token ${{ secrets.CARGO_TOKEN }}

View File

@@ -1,81 +1,76 @@
on: [push, pull_request]
on:
push:
branches:
- main
pull_request:
branches:
- main
name: CI
env:
CI_CARGO_MAKE_VERSION: 0.35.16
jobs:
linux:
name: Linux
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
rust: ["1.59.0", "stable"]
include:
- os: ubuntu-latest
triple: x86_64-unknown-linux-musl
- os: windows-latest
triple: x86_64-pc-windows-msvc
- os: macos-latest
triple: x86_64-apple-darwin
runs-on: ${{ matrix.os }}
steps:
- uses: hecrj/setup-rust-action@50a120e4d34903c2c1383dec0e9b1d349a9cc2b1
with:
rust-version: ${{ matrix.rust }}
components: rustfmt,clippy
- uses: actions/checkout@v3
- name: Install cargo-make on Linux or macOS
if: ${{ runner.os != 'windows' }}
shell: bash
run: |
curl -LO 'https://github.com/sagiegurari/cargo-make/releases/download/${{ env.CI_CARGO_MAKE_VERSION }}/cargo-make-v${{ env.CI_CARGO_MAKE_VERSION }}-${{ matrix.triple }}.zip'
unzip 'cargo-make-v${{ env.CI_CARGO_MAKE_VERSION }}-${{ matrix.triple }}.zip'
cp 'cargo-make-v${{ env.CI_CARGO_MAKE_VERSION }}-${{ matrix.triple }}/cargo-make' ~/.cargo/bin/
cargo make --version
- name: Install cargo-make on Windows
if: ${{ runner.os == 'windows' }}
shell: bash
run: |
# `cargo-make-v0.35.16-{target}/` directory is created on Linux and macOS, but it is not creatd on Windows.
mkdir cargo-make-temporary
cd cargo-make-temporary
curl -LO 'https://github.com/sagiegurari/cargo-make/releases/download/${{ env.CI_CARGO_MAKE_VERSION }}/cargo-make-v${{ env.CI_CARGO_MAKE_VERSION }}-${{ matrix.triple }}.zip'
unzip 'cargo-make-v${{ env.CI_CARGO_MAKE_VERSION }}-${{ matrix.triple }}.zip'
cp cargo-make.exe ~/.cargo/bin/
cd ..
cargo make --version
- name: "Format / Build / Test"
run: cargo make ci
env:
RUST_BACKTRACE: full
lint:
runs-on: ubuntu-latest
strategy:
matrix:
rust: ["1.44.0", "stable"]
steps:
- name: "Install dependencies"
run: sudo apt-get install libncurses5-dev
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
- name: Checkout
if: github.event_name != 'pull_request'
uses: actions/checkout@v3
- name: Checkout
if: github.event_name == 'pull_request'
uses: actions/checkout@v3
with:
profile: default
toolchain: ${{ matrix.rust }}
override: true
- name: "Format"
uses: actions-rs/cargo@v1
ref: ${{ github.event.pull_request.head.sha }}
- name: "Check conventional commits"
uses: crate-ci/committed@master
with:
command: fmt
args: --all -- --check
- name: "Check"
uses: actions-rs/cargo@v1
with:
command: check
args: --examples
- name: "Check (crossterm)"
uses: actions-rs/cargo@v1
with:
command: check
args: --no-default-features --features=crossterm --example crossterm_demo
- name: "Check (rustbox)"
uses: actions-rs/cargo@v1
with:
command: test
args: --no-default-features --features=rustbox --example rustbox_demo
- name: "Check (curses)"
uses: actions-rs/cargo@v1
with:
command: check
args: --no-default-features --features=curses --example curses_demo
- name: "Test"
uses: actions-rs/cargo@v1
with:
command: test
env:
RUST_BACKTRACE: full
- name: "Clippy"
uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-targets --all-features -- -D warnings
windows:
name: Windows
runs-on: windows-latest
strategy:
matrix:
rust: ["1.44.0", "stable"]
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
profile: default
toolchain: ${{ matrix.rust }}
override: true
- name: "Check (crossterm)"
uses: actions-rs/cargo@v1
with:
command: check
args: --no-default-features --features=crossterm --example crossterm_demo
- name: "Test (crossterm)"
uses: actions-rs/cargo@v1
with:
command: test
args: --no-default-features --features=crossterm --tests --examples
env:
RUST_BACKTRACE: full
args: "-vv"
commits: "HEAD"
- name: "Check typos"
uses: crate-ci/typos@master

48
APPS.md Normal file
View File

@@ -0,0 +1,48 @@
# Apps using `ratatui`
| Name | Description | Author |
| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| [adsb_deku/radar](https://github.com/rsadsb/adsb_deku) | Rust ADS-B decoder + TUI radar application | [Rust ADS-B](https://github.com/rsadsb) |
| [bandwhich](https://github.com/imsnif/bandwhich) | Terminal utility for displaying current network utilization by process, connection and remote IP/hostname | [Aram Drevekenin](https://github.com/imsnif) |
| [battleship.rs](https://github.com/deepu105/battleship-rs) | A terminal battleship game in Rust | [Deepu K Sasidharan](https://github.com/deepu105) |
| [bottom](https://github.com/ClementTsang/bottom) | Yet another cross-platform graphical process/system monitor | [Clement Tsang](https://github.com/ClementTsang) |
| [conclusive](https://github.com/mrusme/conclusive) | Command line client for Plausible Analytics | [mrusme](https://github.com/mrusme) |
| [cotp](https://github.com/replydev/cotp) | Trustworthy, encrypted, command-line TOTP/HOTP authenticator app with import functionality | [Reply](https://github.com/replydev) |
| [cube timer](https://github.com/paarthmadan/cube) | A tui-based Rubik's cube timer written in Rust | [Paarth Madan](https://github.com/paarthmadan) |
| [desed](https://github.com/SoptikHa2/desed) | Debugger for Sed: demystify and debug your sed scripts, from comfort of your terminal | [Petr Šťastný](https://github.com/SoptikHa2) |
| [diskonaut](https://github.com/imsnif/diskonaut) | Terminal disk space navigator | [Aram Drevekenin](https://github.com/imsnif) |
| [exhaust](https://github.com/heyrict/exhaust) | Exhaust all your possibilities.. for the next coming exam | [Zhenhui Xie](https://github.com/heyrict) |
| [game-of-life-rs](https://github.com/kachark/game-of-life-rs) | Conway's Game of Life implemented in Rust and visualized with Tui-rs | [kachark](https://github.com/kachark) |
| [gitui](https://github.com/extrawurst/gitui) | Blazing fast terminal-ui for Git written in Rust | [extrawurst](https://github.com/extrawurst) |
| [glicol-cli](https://github.com/glicol/glicol-cli) | Music live coding in terminal | [glicol](https://github.com/glicol) |
| [gpg-tui](https://github.com/orhun/gpg-tui) | Manage your GnuPG keys with ease! | [Orhun Parmaksız](https://github.com/orhun) |
| [gping](https://github.com/orf/gping) | Ping, but with a graph | [Tom Forbes](https://github.com/orf) |
| [joshuto](https://github.com/kamiyaa/joshuto) | Ranger-like terminal file manager written in Rust | [Jeff Zhao](https://github.com/kamiyaa) |
| [kDash](https://github.com/kdash-rs/kdash) | A simple and fast dashboard for Kubernetes | [kdash-rs ](https://github.com/kdash-rs) |
| [kmon](https://github.com/orhun/kmon) | Linux Kernel Manager and Activity Monitor | [Orhun Parmaksız](https://github.com/orhun) |
| [kubectl-watch](https://github.com/imuxin/kubectl-watch) | A kubectl plugin to provide a pretty delta change view of being watched kubernetes resources | [牧心](https://github.com/imuxin) |
| [logss](https://github.com/todoesverso/logss) | A simple command line tool that helps you visualize an input stream of text. | [Victor Rosales](https://github.com/todoesverso) |
| [minesweep](https://github.com/cpcloud/minesweep-rs) | Sweep some mines for fun, and probably not for profit | [Phillip Cloud](https://github.com/cpcloud) |
| [oha](https://github.com/hatoo/oha) | HTTP load generator, inspired by rakyll/hey with tui animation | [hatoo](https://github.com/hatoo) |
| [oxker](https://github.com/mrjackwills/oxker) | a simple tui to view & control docker containers | [Jack Wills](https://github.com/mrjackwills) |
| [oxycards](https://github.com/BrookJeynes/oxycards) | Oxycards is a quiz card application built within the terminal | [Brook Jeynes](https://github.com/BrookJeynes) |
| [poketex](https://github.com/ckaznable/poketex) | A simple pokedex based on TUI | [CK Aznable](https://github.com/ckaznable) |
| [repgrep](https://github.com/acheronfail/repgrep) | An interactive find and replace app powered by ripgrep | [acheronfail](https://github.com/acheronfail) |
| [rrtop](https://github.com/wojciech-zurek/rrtop) | Redis monitoring (top like) app | [Wojciech Żurek](https://github.com/wojciech-zurek) |
| [rust-sadari-cli](https://github.com/24seconds/rust-sadari-cli) | Sadari game based on terminal | [24seconds](https://github.com/24seconds) |
| [rusty-krab-manager](https://github.com/aryakaul/rusty-krab-manager) | Time-management TUI in Rust | [Arya](https://github.com/aryakaul) |
| [spotify-tui](https://github.com/Rigellute/spotify-tui) | Spotify for the terminal written in Rust | [Alexander Keliris](https://github.com/Rigellute) |
| [systeroid](https://github.com/orhun/systeroid) | A more powerful alternative to sysctl(8) with a terminal user interface | [Orhun Parmaksız](https://github.com/orhun) |
| [taskwarrior-tui](https://github.com/kdheepak/taskwarrior-tui) | A terminal user interface for Taskwarrior | [Dheepak Krishnamurthy](https://github.com/kdheepak) |
| [tenere](https://github.com/pythops/tenere) | TUI interface for LLMs written in Rust | [Badr Badri](https://github.com/pythops) |
| [termchat](https://github.com/lemunozm/termchat) | Terminal chat through the LAN with video streaming and file transfer | [Luis Enrique Muñoz Martín](https://github.com/lemunozm) |
| [termscp](https://github.com/veeso/termscp) | A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/S3 | [Christian Visintin](https://github.com/veeso) |
| [tick-rs](https://github.com/tarkah/tickrs) | Realtime ticker data in your terminal | [Cory Forsstrom](https://github.com/tarkah) |
| [trippy](https://github.com/fujiapple852/trippy) | A network diagnostic tool (combines traceroute & ping, like mtr) | [fujiapple852](https://github.com/fujiapple852) |
| [tsuchita](https://github.com/kamiyaa/tsuchita) | Client-server notification center for dbus desktop notifications | [Jeff Zhao](https://github.com/kamiyaa) |
| [tuinance](https://github.com/landchad/tuinance) | Display financial data on the terminal | [bloatoo](https://github.com/bloatoo) |
| [twitch-tui](https://github.com/Xithrius/twitch-tui) | Twitch chat in the terminal. | [Xithrius](https://github.com/Xithrius) |
| [vector](https://vector.dev) | A lightweight, ultra-fast tool for building observability pipelines | [vectordotdev](https://github.com/vectordotdev) |
| [xplr](https://github.com/sayanarijit/xplr) | A hackable, minimal, fast TUI file explorer | [Arijit Basu](https://github.com/sayanarijit/xplr) |
| [ytop](https://github.com/cjbassi/ytop) | A TUI system monitor written in Rust (no longer maintained) | [Caleb Bassi](https://github.com/cjbassi) |
| [zenith](https://github.com/bvaisvil/zenith) | Sort of like top or htop but with zoom-able charts, CPU, GPU, network, and disk usage | [Benjamin Vaisvil](https://github.com/bvaisvil) |

View File

@@ -1,9 +1,303 @@
# Changelog
## To be released
## v0.20.1 - 2023-03-19
### Bug Fixes
- *(style)* Bold needs a bit ([#104](https://github.com/tui-rs-revival/ratatui/issues/104))
### Documentation
- *(apps)* Add "logss" to apps ([#105](https://github.com/tui-rs-revival/ratatui/issues/105))
- *(uncategorized)* Fixup remaining tui references ([#106](https://github.com/tui-rs-revival/ratatui/issues/106))
### Contributors
Thank you so much to everyone that contributed to this release!
- [@joshka](https://github.com/joshka)
- [@todoesverso](https://github.com/todoesverso)
- [@UncleScientist](https://github.com/UncleScientist)
## v0.20.0 - 2023-03-19
This marks the first release of `ratatui`, a community-maintained fork of [tui](https://github.com/fdehau/tui-rs).
The purpose of this release is to include **bug fixes** and **small changes** into the repository thus **no new features** are added. We have transferred all the pull requests from the original repository and worked on the low hanging ones to incorporate them in this "maintenance" release.
Here is a list of changes:
### Features
- *(cd)* Add continuous deployment workflow ([#93](https://github.com/tui-rs-revival/ratatui/issues/93))
- *(ci)* Add MacOS to CI ([#60](https://github.com/tui-rs-revival/ratatui/issues/60))
- *(widget)* Add `offset()` to `TableState` ([#10](https://github.com/tui-rs-revival/ratatui/issues/10))
- *(widget)* Add `width()` to ListItem ([#17](https://github.com/tui-rs-revival/ratatui/issues/17))
### Bug Fixes
- *(ci)* Test MSRV compatibility on CI ([#85](https://github.com/tui-rs-revival/ratatui/issues/85))
- *(ci)* Bump Rust version to 1.63.0 ([#80](https://github.com/tui-rs-revival/ratatui/issues/80))
- *(ci)* Use env for the cargo-make version ([#76](https://github.com/tui-rs-revival/ratatui/issues/76))
- *(ci)* Fix deprecation warnings on CI ([#58](https://github.com/tui-rs-revival/ratatui/issues/58))
- *(doc)* Add 3rd party libraries accidentally removed at #21 ([#61](https://github.com/tui-rs-revival/ratatui/issues/61))
- *(widget)* List should not ignore empty string items ([#42](https://github.com/tui-rs-revival/ratatui/issues/42)) [**breaking**]
- *(uncategorized)* Cassowary/layouts: add extra constraints for fixing Min(v)/Max(v) combination. ([#31](https://github.com/tui-rs-revival/ratatui/issues/31))
- *(uncategorized)* Fix user_input example double key press registered on windows
- *(uncategorized)* Ignore zero-width symbol on rendering `Paragraph`
- *(uncategorized)* Fix typos ([#45](https://github.com/tui-rs-revival/ratatui/issues/45))
- *(uncategorized)* Fix typos ([#47](https://github.com/tui-rs-revival/ratatui/issues/47))
### Refactor
- *(style)* Make bitflags smaller ([#13](https://github.com/tui-rs-revival/ratatui/issues/13))
### Documentation
- *(apps)* Move 'apps using ratatui' to dedicated file ([#98](https://github.com/tui-rs-revival/ratatui/issues/98)) ([#99](https://github.com/tui-rs-revival/ratatui/issues/99))
- *(canvas)* Add documentation for x_bounds, y_bounds ([#35](https://github.com/tui-rs-revival/ratatui/issues/35))
- *(contributing)* Specify the use of unsafe for optimization ([#67](https://github.com/tui-rs-revival/ratatui/issues/67))
- *(github)* Remove pull request template ([#68](https://github.com/tui-rs-revival/ratatui/issues/68))
- *(readme)* Update crate status badge ([#102](https://github.com/tui-rs-revival/ratatui/issues/102))
- *(readme)* Small edits before first release ([#101](https://github.com/tui-rs-revival/ratatui/issues/101))
- *(readme)* Add install instruction and update title ([#100](https://github.com/tui-rs-revival/ratatui/issues/100))
- *(readme)* Add systeroid to application list ([#92](https://github.com/tui-rs-revival/ratatui/issues/92))
- *(readme)* Add glicol-cli to showcase list ([#95](https://github.com/tui-rs-revival/ratatui/issues/95))
- *(readme)* Add oxker to application list ([#74](https://github.com/tui-rs-revival/ratatui/issues/74))
- *(readme)* Add app kubectl-watch which uses tui ([#73](https://github.com/tui-rs-revival/ratatui/issues/73))
- *(readme)* Add poketex to 'apps using tui' in README ([#64](https://github.com/tui-rs-revival/ratatui/issues/64))
- *(readme)* Update README.md ([#39](https://github.com/tui-rs-revival/ratatui/issues/39))
- *(readme)* Update README.md ([#40](https://github.com/tui-rs-revival/ratatui/issues/40))
- *(readme)* Clarify README.md fork status update
- *(uncategorized)* Fix: fix typos ([#90](https://github.com/tui-rs-revival/ratatui/issues/90))
- *(uncategorized)* Update to build more backends ([#81](https://github.com/tui-rs-revival/ratatui/issues/81))
- *(uncategorized)* Expand "Apps" and "Third-party" sections ([#21](https://github.com/tui-rs-revival/ratatui/issues/21))
- *(uncategorized)* Add tui-input and update xplr in README.md
- *(uncategorized)* Add hncli to list of applications made with tui-rs ([#41](https://github.com/tui-rs-revival/ratatui/issues/41))
- *(uncategorized)* Updated readme and contributing guide with updates about the fork ([#46](https://github.com/tui-rs-revival/ratatui/issues/46))
### Performance
- *(layout)* Better safe shared layout cache ([#62](https://github.com/tui-rs-revival/ratatui/issues/62))
### Miscellaneous Tasks
- *(cargo)* Update project metadata ([#94](https://github.com/tui-rs-revival/ratatui/issues/94))
- *(ci)* Integrate `typos` for checking typos ([#91](https://github.com/tui-rs-revival/ratatui/issues/91))
- *(ci)* Change the target branch to main ([#79](https://github.com/tui-rs-revival/ratatui/issues/79))
- *(ci)* Re-enable clippy on CI ([#59](https://github.com/tui-rs-revival/ratatui/issues/59))
- *(uncategorized)* Integrate `committed` for checking conventional commits ([#77](https://github.com/tui-rs-revival/ratatui/issues/77))
- *(uncategorized)* Update `rust-version` to 1.59 in Cargo.toml ([#57](https://github.com/tui-rs-revival/ratatui/issues/57))
- *(uncategorized)* Update deps ([#51](https://github.com/tui-rs-revival/ratatui/issues/51))
- *(uncategorized)* Fix typo in layout.rs ([#619](https://github.com/tui-rs-revival/ratatui/issues/619))
- *(uncategorized)* Add apps using `tui`
### Contributors
Thank you so much to everyone that contributed to this release!
- [@orhun](https://github.com/orhun)
- [@mindoodoo](https://github.com/mindoodoo)
- [@sayanarijit](https://github.com/sayanarijit)
- [@Owletti](https://github.com/Owletti)
- [@UncleScientist](https://github.com/UncleScientist)
- [@rhysd](https://github.com/rhysd)
- [@ckaznable](https://github.com/ckaznable)
- [@imuxin](https://github.com/imuxin)
- [@mrjackwills](https://github.com/mrjackwills)
- [@conradludgate](https://github.com/conradludgate)
- [@kianmeng](https://github.com/kianmeng)
- [@chaosprint](https://github.com/chaosprint)
And most importantly, special thanks to [Florian Dehau](https://github.com/fdehau) for creating this awesome library 💖 We look forward to building on the strong foundations that the original crate laid out.
## v0.19.0 - 2022-08-14
### Features
* Bump `crossterm` to `0.25`
## 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 highlight 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
* Update `crossterm` to `0.19`.
* Update `rand` to `0.8`.
* Add a read-only view of the terminal state after the draw call (#440).
### Fixes
* Remove compile warning in `TestBackend::assert_buffer` (#466).
## v0.14.0 - 2021-01-01
### Breaking changes
#### New API for the Table widget
The `Table` widget got a lot of improvements that should make it easier to work with:
* It should not longer panic when rendered on small areas.
* `Row`s are now a collection of `Cell`s, themselves wrapping a `Text`. This means you can style
the entire `Table`, an entire `Row`, an entire `Cell` and rely on the styling capabilities of
`Text` to get full control over the look of your `Table`.
* `Row`s can have multiple lines.
* The header is now optional and is just another `Row` always visible at the top.
* `Row`s can have a bottom margin.
* The header alignment is no longer off when an item is selected.
Taking the example of the code in `examples/demo/ui.rs`, this is what you may have to change:
```diff
let failure_style = Style::default()
.fg(Color::Red)
.add_modifier(Modifier::RAPID_BLINK | Modifier::CROSSED_OUT);
- let header = ["Server", "Location", "Status"];
let rows = app.servers.iter().map(|s| {
let style = if s.status == "Up" {
up_style
} else {
failure_style
};
- Row::StyledData(vec![s.name, s.location, s.status].into_iter(), style)
+ Row::new(vec![s.name, s.location, s.status]).style(style)
});
- let table = Table::new(header.iter(), rows)
+ let table = Table::new(rows)
+ .header(
+ Row::new(vec!["Server", "Location", "Status"])
+ .style(Style::default().fg(Color::Yellow))
+ .bottom_margin(1),
+ )
.block(Block::default().title("Servers").borders(Borders::ALL))
- .header_style(Style::default().fg(Color::Yellow))
.widths(&[
Constraint::Length(15),
Constraint::Length(15),
```
Here, we had to:
- Change the way we construct [`Row`](https://docs.rs/tui/*/tui/widgets/struct.Row.html) which is no
longer an `enum` but a `struct`. It accepts anything that can be converted to an iterator of things
that can be converted to a [`Cell`](https://docs.rs/tui/*/tui/widgets/struct.Cell.html)
- The header is no longer a required parameter so we use
[`Table::header`](https://docs.rs/tui/*/tui/widgets/struct.Table.html#method.header) to set it.
`Table::header_style` has been removed since the style can be directly set using
[`Row::style`](https://docs.rs/tui/*/tui/widgets/struct.Row.html#method.style). In addition, we want
to preserve the old margin between the header and the rest of the rows so we add a bottom margin to
the header using
[`Row::bottom_margin`](https://docs.rs/tui/*/tui/widgets/struct.Row.html#method.bottom_margin).
You may want to look at the documentation of the different types to get a better understanding:
- [`Table`](https://docs.rs/tui/*/tui/widgets/struct.Table.html)
- [`Row`](https://docs.rs/tui/*/tui/widgets/struct.Row.html)
- [`Cell`](https://docs.rs/tui/*/tui/widgets/struct.Cell.html)
### Fixes
- Fix handling of Non Breaking Space (NBSP) in wrapped text in `Paragraph` widget.
### Features
- Add `Style::reset` to create a `Style` resetting all styling properties when applied.
- Add an option to render the `Gauge` widget with unicode blocks.
- Manage common project tasks with `cargo-make` rather than `make` for easier on-boarding.
## v0.13.0 - 2020-11-14
### Features
* Add `LineGauge` widget which is a more compact variant of the existing `Gauge`.
* Bump `crossterm` to 0.18
### Fixes
* Take into account the borders of the `Table` widget when the widths of columns is controlled by
`Percentage` and `Ratio` constraints.
## v0.12.0 - 2020-09-27
### Features
* Make it easier to work with string with multiple lines in `Text` (#361).
### Fixes
* Fix a style leak in `Graph` so components drawn on top of the plotted data (i.e legend and axis
titles) are not affected by the style of the `Dataset`s (#388).
* Make sure `BarChart` shows bars with the max height only when the plotted data is actually equal
to the max (#383).
## v0.11.0 - 2020-09-20
### Features
* Add the dot character as a new type of canvas marker (#350).
* Support more style modifiers on Windows (#368).
### Fixes
* Clearing the terminal through `Terminal::clear` will cause the whole UI to be redrawn (#380).
* Fix incorrect output when the first diff to draw is on the second cell of the terminal (#347).
## v0.10.0 - 2020-07-17
@@ -91,7 +385,7 @@ In this new release, you may now write this as:
```rust
Block::default()
.style(Style::default().bg(Color::Green))
// The style is not overidden anymore, we simply add new style rule for the title.
// The style is not overridden anymore, we simply add new style rule for the title.
.title(Span::styled("My title", Style::default().add_modifier(Modifier::BOLD)))
```
@@ -99,9 +393,9 @@ In addition, the crate now provides a method `patch` to combine two styles into
rules:
```rust
let style = Style::default().modifer(Modifier::BOLD);
let style = Style::default().modifier(Modifier::BOLD);
let style = style.patch(Style::default().add_modifier(Modifier::ITALIC));
// style.modifer == Modifier::BOLD | Modifier::ITALIC, the modifier has been enriched not overidden
// style.modifier == Modifier::BOLD | Modifier::ITALIC, the modifier has been enriched not overridden
```
- `Style::modifier` has been removed in favor of `Style::add_modifier` and `Style::remove_modifier`.
@@ -419,7 +713,7 @@ let style = Style::default().add_modifier(Modifier::ITALIC | Modifier::BOLD);
### Bug Fixes
* Ensure correct behavoir of the alternate screens with the `Crossterm` backend.
* Ensure correct behavior of the alternate screens with the `Crossterm` backend.
* Fix out of bounds panic when two `Buffer` are merged.
## v0.4.0 - 2019-02-03
@@ -488,7 +782,7 @@ additional `termion` features.
* Replace `Item` by a generic and flexible `Text` that can be used in both
`Paragraph` and `List` widgets.
* Remove unecessary borrows on `Style`.
* Remove unnecessary borrows on `Style`.
## v0.3.0-beta.0 - 2018-09-04
@@ -511,7 +805,7 @@ widgets on the given `Frame`
* All widgets use the consumable builder pattern
* `SelectableList` can have no selected item and the highlight symbol is hidden
in this case
* Remove markup langage inside `Paragraph`. `Paragraph` now expects an iterator
* Remove markup language inside `Paragraph`. `Paragraph` now expects an iterator
of `Text` items
## v0.2.3 - 2018-06-09
@@ -519,7 +813,7 @@ of `Text` items
### Features
* Add `start_corner` option for `List`
* Add more text aligment options for `Paragraph`
* Add more text alignment options for `Paragraph`
## v0.2.2 - 2018-05-06

61
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,61 @@
# Fork Status
## Pull Requests
**All** pull requests opened on the original repository have been imported. We'll be going through any open PRs in a timely manner, starting with the **smallest bug fixes and README updates**. If you have an open PR make sure to let us know about it on our [discord](https://discord.gg/pMCEU9hNEj) as it helps to know you are still active.
## Issues
We have been unsuccessful in importing all issues opened on the previous repository.
For that reason, anyone wanting to **work on or discuss** an issue will have to follow the following workflow :
- Recreate the issue
- Start by referencing the **original issue**: ```Referencing issue #[<issue number>](<original issue link>)```
- Then, paste the original issues **opening** text
You can then resume the conversation by replying to this new issue you have created.
### Closing Issues
If you close an issue that you have "imported" to this fork, please make sure that you add the issue to the **CLOSED_ISSUES.md**. This will enable us to keep track of which issues have been closed from the original repo, in case we are able to have the original repository transferred.
# Contributing
## Implementation Guidelines
### Use of unsafe for optimization purposes
**Do not** use unsafe to achieve better performances. This is subject to change, [see.](https://github.com/tui-rs-revival/tui-rs-revival/discussions/66)
The only exception to this rule is if it's to fix **reproducible slowness.**
## Building
[cargo-make]: https://github.com/sagiegurari/cargo-make "cargo-make"
`ratatui` 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`.
## :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).
- The tests (docs, lib, tests and examples) should pass.
- 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.
## 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 usual doc and unit tests, one of the most valuable test you can write for `ratatui` is a test against 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,126 +1,109 @@
[package]
name = "tui"
version = "0.10.0"
name = "ratatui"
version = "0.20.1"
authors = ["Florian Dehau <work@fdehau.com>"]
description = """
A library to build rich terminal user interfaces or dashboards
"""
documentation = "https://docs.rs/tui/0.10.0/tui/"
documentation = "https://docs.rs/ratatui/latest/ratatui/"
keywords = ["tui", "terminal", "dashboard"]
repository = "https://github.com/fdehau/tui-rs"
repository = "https://github.com/tui-rs-revival/ratatui"
readme = "README.md"
license = "MIT"
exclude = ["assets/*", ".github"]
exclude = ["assets/*", ".github", "Makefile.toml", "CONTRIBUTING.md", "*.log", "tags"]
autoexamples = true
edition = "2018"
edition = "2021"
rust-version = "1.59.0"
[badges]
[features]
default = ["termion"]
curses = ["easycurses", "pancurses"]
default = ["crossterm"]
all-widgets = ["widget-calendar"]
widget-calendar = ["time"]
[package.metadata.docs.rs]
all-features = true
[dependencies]
bitflags = "1.0"
bitflags = "1.3"
cassowary = "0.3"
unicode-segmentation = "1.2"
unicode-segmentation = "1.10"
unicode-width = "0.1"
termion = { version = "1.5", optional = true }
rustbox = { version = "0.11", optional = true }
crossterm = { version = "0.17", 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"]}
termion = { version = "2.0", optional = true }
crossterm = { version = "0.26", optional = true }
serde = { version = "1", optional = true, features = ["derive"]}
time = { version = "0.3.11", optional = true, features = ["local-offset"]}
[dev-dependencies]
rand = "0.7"
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"]
indoc = "2.0"
[[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 = "calendar"
required-features = ["crossterm", "widget-calendar"]
[[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"]
[[example]]
name = "inline"
required-features = ["crossterm"]

116
Makefile
View File

@@ -1,116 +0,0 @@
SHELL=/bin/bash
# ================================ Cargo ======================================
RUST_CHANNEL ?= stable
CARGO_FLAGS =
RUSTUP_INSTALLED = $(shell command -v rustup 2> /dev/null)
TEST_FILTER ?=
ifndef RUSTUP_INSTALLED
CARGO = cargo
else
ifdef CI
CARGO = cargo
else
CARGO = rustup run $(RUST_CHANNEL) cargo
endif
endif
# ================================ Help =======================================
.PHONY: help
help: ## Print all the available commands
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
# =============================== Build =======================================
.PHONY: check
check: ## Validate the project code
$(CARGO) check
.PHONY: build
build: ## Build the project in debug mode
$(CARGO) build $(CARGO_FLAGS)
.PHONY: release
release: CARGO_FLAGS += --release
release: build ## Build the project in release mode
# ================================ Lint =======================================
.PHONY: lint
lint: fmt clippy ## Lint project files
.PHONY: fmt
fmt: ## Check the format of the source code
cargo fmt --all -- --check
.PHONY: clippy
clippy: ## Check the style of the source code and catch common errors
$(CARGO) clippy --all-targets --all-features -- -D warnings
# ================================ Test =======================================
.PHONY: test
test: ## Run the tests
$(CARGO) test --all-features $(TEST_FILTER)
# =============================== Examples ====================================
.PHONY: build-examples
build-examples: ## Build all examples
@$(CARGO) build --release --examples --all-features
.PHONY: run-examples
run-examples: build-examples ## Run all examples
@for file in examples/*.rs; do \
name=$$(basename $${file/.rs/}); \
$(CARGO) run --all-features --release --example $$name; \
done;
# ================================ Doc ========================================
.PHONY: doc
doc: RUST_CHANNEL = nightly
doc: ## Build the documentation (available at ./target/doc)
$(CARGO) doc
# ================================= Watch =====================================
# Requires watchman and watchman-make (https://facebook.github.io/watchman/docs/install.html)
.PHONY: watch
watch: ## Watch file changes and build the project if any
watchman-make -p 'src/**/*.rs' -t check build
.PHONY: watch-test
watch-test: ## Watch files changes and run the tests if any
watchman-make -p 'src/**/*.rs' 'tests/**/*.rs' 'examples/**/*.rs' -t test
.PHONY: watch-doc
watch-doc: RUST_CHANNEL = nightly
watch-doc: ## Watch file changes and rebuild the documentation if any
$(CARGO) watch -x doc -x 'test --doc'
# ================================= Pipelines =================================
.PHONY: stable
stable: RUST_CHANNEL = stable
stable: build lint test ## Run build and tests for stable
.PHONY: beta
beta: RUST_CHANNEL = beta
beta: build lint test ## Run build and tests for beta
.PHONY: nightly
nightly: RUST_CHANNEL = nightly
nightly: build lint test ## Run build, lint and tests for nightly

160
Makefile.toml Normal file
View File

@@ -0,0 +1,160 @@
[config]
skip_core_tasks = true
[tasks.ci]
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"
args = [
"fmt",
"--all",
"--",
"--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",
"--features",
"${TUI_FEATURES}",
"--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",
"--features",
"${TUI_FEATURES}",
"--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}",
"--",
"-D",
"warnings",
]
[tasks.test-crossterm]
env = { TUI_FEATURES = "serde,crossterm,all-widgets" }
run_task = "test"
[tasks.test-termion]
env = { TUI_FEATURES = "serde,termion,all-widgets" }
run_task = "test"
[tasks.test]
command = "cargo"
condition = { env_set = ["TUI_FEATURES"] }
args = [
"test",
"--no-default-features",
"--features",
"${TUI_FEATURES}",
"--lib",
"--tests",
"--examples",
]
[tasks.test-doc]
command = "cargo"
args = [
"test",
"--doc",
]
[tasks.run-example]
private = true
condition = { env_set = ["TUI_EXAMPLE_NAME"] }
command = "cargo"
args = [
"run",
"--release",
"--example",
"${TUI_EXAMPLE_NAME}"
]
[tasks.build-examples]
command = "cargo"
args = [
"build",
"--examples",
"--release"
]
[tasks.run-examples]
dependencies = ["build-examples"]
script = '''
#!@duckscript
files = glob_array ./examples/*.rs
for file in ${files}
name = basename ${file}
name = substring ${name} -3
set_env TUI_EXAMPLE_NAME ${name}
cm_run_task run-example
end
'''

146
README.md
View File

@@ -1,25 +1,42 @@
# tui-rs
# ratatui
[![Build Status](https://github.com/fdehau/tui-rs/workflows/CI/badge.svg)](https://github.com/fdehau/tui-rs/actions?query=workflow%3ACI+)
[![Crate Status](https://img.shields.io/crates/v/tui.svg)](https://crates.io/crates/tui)
[![Docs Status](https://docs.rs/tui/badge.svg)](https://docs.rs/crate/tui/)
An actively maintained `tui-rs` fork.
[![Build Status](https://github.com/tui-rs-revival/ratatui/workflows/CI/badge.svg)](https://github.com/tui-rs-revival/ratatui/actions?query=workflow%3ACI+)
[![Crate Status](https://img.shields.io/crates/v/ratatui.svg)](https://crates.io/crates/ratatui)
[![Docs Status](https://docs.rs/ratatui/badge.svg)](https://docs.rs/crate/ratatui/)
<img src="./assets/demo.gif" alt="Demo cast under Linux Termite with Inconsolata font 12pt">
`tui-rs` is a [Rust](https://www.rust-lang.org) library to build rich terminal
# Install
```toml
[dependencies]
tui = { package = "ratatui" }
```
# What is this fork?
This fork was created to continue maintenance on the original TUI project. The original maintainer had created an [issue](https://github.com/fdehau/tui-rs/issues/654) explaining how he couldn't find time to continue development, which led to us creating this fork.
With that in mind, **we the community** look forward to continuing the work started by [**Florian Dehau.**](https://github.com/fdehau) :rocket:
In order to organize ourselves, we currently use a [discord server](https://discord.gg/pMCEU9hNEj), feel free to join and come chat ! There are also plans to implement a [matrix](https://matrix.org/) bridge in the near future.
**Discord is not a MUST to contribute,** we follow a pretty standard github centered open source workflow keeping the most important conversations on github, open an issue or PR and it will be addressed. :smile:
Please make sure you read the updated contributing guidelines, especially if you are interested in working on a PR or issue opened in the previous repository.
# Introduction
`ratatui` is a [Rust](https://www.rust-lang.org) library to build rich terminal
user interfaces and dashboards. It is heavily inspired by the `Javascript`
library [blessed-contrib](https://github.com/yaronn/blessed-contrib) and the
`Go` library [termui](https://github.com/gizak/termui).
The library itself supports four different backends to draw to the terminal. You
can either choose from:
The library supports multiple backends:
- [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.
- [crossterm](https://github.com/crossterm-rs/crossterm) [default]
- [termion](https://github.com/ticki/termion)
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
@@ -32,84 +49,91 @@ comes from the terminal emulator than the library itself.
Moreover, the library does not provide any input handling nor any event system and
you may rely on the previously cited libraries to achieve such features.
### Rust version requirements
## Rust version requirements
Since version 0.10.0, `tui` requires **rustc version 1.44.0 or greater**.
Since version 0.17.0, `ratatui` requires **rustc version 1.59.0 or greater**.
### [Documentation](https://docs.rs/tui)
# Documentation
### Demo
The documentation can be found on [docs.rs.](https://docs.rs/ratatui)
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:
# Demo
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/tui-rs-revival/ratatui/blob/main/examples/demo/ui.rs) while the
application state is in [examples/demo/app.rs](https://github.com/tui-rs-revival/ratatui/blob/main/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
# Widgets
## Built in
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/tui-rs-revival/ratatui/blob/main/examples/block.rs)
- [Gauge](https://github.com/tui-rs-revival/ratatui/blob/main/examples/gauge.rs)
- [Sparkline](https://github.com/tui-rs-revival/ratatui/blob/main/examples/sparkline.rs)
- [Chart](https://github.com/tui-rs-revival/ratatui/blob/main/examples/chart.rs)
- [BarChart](https://github.com/tui-rs-revival/ratatui/blob/main/examples/barchart.rs)
- [List](https://github.com/tui-rs-revival/ratatui/blob/main/examples/list.rs)
- [Table](https://github.com/tui-rs-revival/ratatui/blob/main/examples/table.rs)
- [Paragraph](https://github.com/tui-rs-revival/ratatui/blob/main/examples/paragraph.rs)
- [Canvas (with line, point cloud, map)](https://github.com/tui-rs-revival/ratatui/blob/main/examples/canvas.rs)
- [Tabs](https://github.com/tui-rs-revival/ratatui/blob/main/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`.
Click on each item to see the source of the example. Run the examples with with
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
### Third-party libraries, bootstrapping templates and widgets
* [tui-logger](https://github.com/gin66/tui-logger)
- [ansi-to-tui](https://github.com/uttarayan21/ansi-to-tui) — Convert ansi colored text to `tui::text::Text`
- [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to `tui::style::Color`
- [rust-tui-template](https://github.com/orhun/rust-tui-template) — A template for bootstrapping a Rust TUI application with Tui-rs & crossterm
- [simple-tui-rs](https://github.com/pmsanford/simple-tui-rs) — A simple example tui-rs app
- [tui-builder](https://github.com/jkelleyrtp/tui-builder) — Batteries-included MVC framework for Tui-rs + Crossterm apps
- [tui-clap](https://github.com/kegesch/tui-clap-rs) — Use clap-rs together with Tui-rs
- [tui-log](https://github.com/kegesch/tui-log-rs) — Example of how to use logging with Tui-rs
- [tui-logger](https://github.com/gin66/tui-logger) — Logger and Widget for Tui-rs
- [tui-realm](https://github.com/veeso/tui-realm) — Tui-rs framework to build stateful applications with a React/Elm inspired approach
- [tui-realm-treeview](https://github.com/veeso/tui-realm-treeview) — Treeview component for Tui-realm
- [tui tree widget](https://github.com/EdJoPaTo/tui-rs-tree-widget) — Tree Widget for Tui-rs
- [tui-windows](https://github.com/markatk/tui-windows-rs) — Tui-rs abstraction to handle multiple windows and their rendering
- [tui-textarea](https://github.com/rhysd/tui-textarea): Simple yet powerful multi-line text editor widget supporting several key shortcuts, undo/redo, text search, etc.
- [tui-rs-tree-widgets](https://github.com/EdJoPaTo/tui-rs-tree-widget): Widget for tree data structures.
- [tui-input](https://github.com/sayanarijit/tui-input): TUI input library supporting multiple backends and tui-rs.
### Apps using tui
# Apps
* [spotify-tui](https://github.com/Rigellute/spotify-tui)
* [bandwhich](https://github.com/imsnif/bandwhich)
* [kmon](https://github.com/orhun/kmon)
* [ytop](https://github.com/cjbassi/ytop)
* [zenith](https://github.com/bvaisvil/zenith)
* [bottom](https://github.com/ClementTsang/bottom)
* [oha](https://github.com/hatoo/oha)
* [gitui](https://github.com/extrawurst/gitui)
* [rust-sadari-cli](https://github.com/24seconds/rust-sadari-cli)
* [desed](https://github.com/SoptikHa2/desed)
* [diskonaut](https://github.com/imsnif/diskonaut)
Check out the list of [close to 40 apps](./APPS.md) using `ratatui`!
### Alternatives
# Alternatives
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an
alternative solution to build text user interfaces in Rust.
## License
# Acknowledgements
Special thanks to [**Pavel Fomchenkov**](https://github.com/nawok) for his work in designing **an awesome logo** for the ratatui project and tui-rs-revival organization.
# License
[MIT](LICENSE)

10
RELEASE.md Normal file
View File

@@ -0,0 +1,10 @@
# Creating a Release
[crates.io](https://crates.io/crates/ratatui) releases are automated via [GitHub actions](.github/workflows/cd.yml) and triggered by pushing a tag.
1. Bump the version in [Cargo.toml](Cargo.toml).
2. Ensure [CHANGELOG.md](CHANGELOG.md) is updated. [git-cliff](https://github.com/orhun/git-cliff) can be used for generating the entries.
3. Commit and push the changes.
4. Create a new tag: `git tag -a v[X.Y.Z]`
5. Push the tag: `git push --tags`
6. Wait for [Continuous Deployment](https://github.com/tui-rs-revival/ratatui/actions) workflow to finish.

87
cliff.toml Normal file
View File

@@ -0,0 +1,87 @@
# configuration file for git-cliff
# see https://github.com/orhun/git-cliff#configuration-file
[changelog]
# changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://tera.netlify.app/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits
| filter(attribute="scope")
| sort(attribute="scope") %}
- *({{commit.scope}})* {{ commit.message | upper_first }}{% if commit.breaking %} [**breaking**]{% endif %}
{%- endfor -%}
{% raw %}\n{% endraw %}\
{%- for commit in commits %}
{%- if commit.scope -%}
{% else -%}
- *(uncategorized)* {{ commit.message | upper_first }}{% if commit.breaking %} [**breaking**]{% endif %}
{% endif -%}
{% endfor -%}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
<!-- generated by git-cliff -->
"""
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/tui-rs-revival/ratatui/issues/${2}))"},
{ pattern = '(better safe shared layout cache)', replace = "perf(layout): ${1}" },
{ pattern = '(Clarify README.md)', replace = "docs(readme): ${1}" },
{ pattern = '(Update README.md)', replace = "docs(readme): ${1}" },
{ pattern = '(fix typos|Fix typos)', replace = "fix: ${1}" }
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "<!-- 00 -->Features"},
{ message = "^[fF]ix", group = "<!-- 01 -->Bug Fixes"},
{ message = "^refactor", group = "<!-- 02 -->Refactor"},
{ message = "^doc", group = "<!-- 03 -->Documentation"},
{ message = "^perf", group = "<!-- 04 -->Performance"},
{ message = "^style", group = "<!-- 05 -->Styling"},
{ message = "^test", group = "<!-- 06 -->Testing"},
{ message = "^chore\\(release\\): prepare for", skip = true},
{ message = "^chore\\(pr\\)", skip = true},
{ message = "^chore\\(pull\\)", skip = true},
{ message = "^chore", group = "<!-- 07 -->Miscellaneous Tasks"},
{ body = ".*security", group = "<!-- 08 -->Security"},
{ message = "^build", group = "<!-- 09 -->Build"},
{ message = "^ci", group = "<!-- 10 -->Continuous Integration"},
{ message = "^revert", group = "<!-- 11 -->Reverted Commits"},
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers
filter_commits = false
# glob pattern for matching git tags
tag_pattern = "v[0-9]*"
# regex for skipping tags
skip_tags = "v0.1.0-rc.1"
# regex for ignoring tags
ignore_tags = ""
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "newest"

18
committed.toml Normal file
View File

@@ -0,0 +1,18 @@
# configuration for https://github.com/crate-ci/committed
# https://www.conventionalcommits.org
style="conventional"
# disallow merge commits
merge_commit = false
# subject is not required to be capitalized
subject_capitalized = false
# subject should start with an imperative verb
imperative_subject = true
# subject should not end with a punctuation
subject_not_punctuated = true
# disable line length
line_length = 0
# disable subject length
subject_length = 0
# default allowed_types [ "chore", "docs", "feat", "fix", "perf", "refactor", "style", "test" ]
allowed_types = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test" ]

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 tui::{
backend::TermionBackend,
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{BarChart, Block, Borders},
Terminal,
Frame, Terminal,
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
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,137 @@
#[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,
layout::{Constraint, Direction, Layout},
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::Span,
widgets::{Block, BorderType, Borders},
Terminal,
widgets::{Block, BorderType, Borders, Padding, Paragraph},
Frame, Terminal,
};
use std::{error::Error, io};
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)
.padding(Padding {
left: 4,
right: 4,
top: 2,
bottom: 2,
});
let text = Paragraph::new("text inside padded block").block(block);
f.render_widget(text, 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)
.padding(Padding::uniform(1));
let inner_block = Block::default()
.title("Block inside padded block")
.borders(Borders::ALL);
let inner_area = block.inner(bottom_chunks[1]);
f.render_widget(block, bottom_chunks[1]);
f.render_widget(inner_block, inner_area);
}

286
examples/calendar.rs Normal file
View File

@@ -0,0 +1,286 @@
use std::{error::Error, io, rc::Rc};
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
Frame, Terminal,
};
use time::{Date, Month, OffsetDateTime};
use ratatui::widgets::calendar::{CalendarEventStore, DateStyler, Monthly};
fn main() -> Result<(), Box<dyn Error>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
loop {
let _ = terminal.draw(|f| draw(f));
if let Event::Key(key) = event::read()? {
#[allow(clippy::single_match)]
match key.code {
KeyCode::Char(_) => {
break;
}
_ => {}
};
}
}
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
Ok(())
}
fn draw<B: Backend>(f: &mut Frame<B>) {
let app_area = f.size();
let calarea = Rect {
x: app_area.x + 1,
y: app_area.y + 1,
height: app_area.height - 1,
width: app_area.width - 1,
};
let mut start = OffsetDateTime::now_local()
.unwrap()
.date()
.replace_month(Month::January)
.unwrap()
.replace_day(1)
.unwrap();
let list = make_dates(start.year());
for chunk in split_rows(&calarea)
.iter()
.flat_map(|row| split_cols(row).to_vec())
{
let cal = cals::get_cal(start.month(), start.year(), &list);
f.render_widget(cal, chunk);
start = start.replace_month(start.month().next()).unwrap();
}
}
fn split_rows(area: &Rect) -> Rc<[Rect]> {
let list_layout = Layout::default()
.direction(Direction::Vertical)
.margin(0)
.constraints(
[
Constraint::Percentage(33),
Constraint::Percentage(33),
Constraint::Percentage(33),
]
.as_ref(),
);
list_layout.split(*area)
}
fn split_cols(area: &Rect) -> Rc<[Rect]> {
let list_layout = Layout::default()
.direction(Direction::Horizontal)
.margin(0)
.constraints(
[
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
]
.as_ref(),
);
list_layout.split(*area)
}
fn make_dates(current_year: i32) -> CalendarEventStore {
let mut list = CalendarEventStore::today(
Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Blue),
);
// Holidays
let holiday_style = Style::default()
.fg(Color::Red)
.add_modifier(Modifier::UNDERLINED);
// new year's
list.add(
Date::from_calendar_date(current_year, Month::January, 1).unwrap(),
holiday_style,
);
// next new_year's for December "show surrounding"
list.add(
Date::from_calendar_date(current_year + 1, Month::January, 1).unwrap(),
holiday_style,
);
// groundhog day
list.add(
Date::from_calendar_date(current_year, Month::February, 2).unwrap(),
holiday_style,
);
// april fool's
list.add(
Date::from_calendar_date(current_year, Month::April, 1).unwrap(),
holiday_style,
);
// earth day
list.add(
Date::from_calendar_date(current_year, Month::April, 22).unwrap(),
holiday_style,
);
// star wars day
list.add(
Date::from_calendar_date(current_year, Month::May, 4).unwrap(),
holiday_style,
);
// festivus
list.add(
Date::from_calendar_date(current_year, Month::December, 23).unwrap(),
holiday_style,
);
// new year's eve
list.add(
Date::from_calendar_date(current_year, Month::December, 31).unwrap(),
holiday_style,
);
// seasons
let season_style = Style::default()
.fg(Color::White)
.bg(Color::Yellow)
.add_modifier(Modifier::UNDERLINED);
// spring equinox
list.add(
Date::from_calendar_date(current_year, Month::March, 22).unwrap(),
season_style,
);
// summer solstice
list.add(
Date::from_calendar_date(current_year, Month::June, 21).unwrap(),
season_style,
);
// fall equinox
list.add(
Date::from_calendar_date(current_year, Month::September, 22).unwrap(),
season_style,
);
list.add(
Date::from_calendar_date(current_year, Month::December, 21).unwrap(),
season_style,
);
list
}
mod cals {
use super::*;
pub(super) fn get_cal<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
use Month::*;
match m {
May => example1(m, y, es),
June => example2(m, y, es),
July => example3(m, y, es),
December => example3(m, y, es),
February => example4(m, y, es),
November => example5(m, y, es),
_ => default(m, y, es),
}
}
fn default<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
let default_style = Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Rgb(50, 50, 50));
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
.show_month_header(Style::default())
.default_style(default_style)
}
fn example1<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
let default_style = Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Rgb(50, 50, 50));
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
.show_surrounding(default_style)
.default_style(default_style)
.show_month_header(Style::default())
}
fn example2<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
let header_style = Style::default()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::DIM)
.fg(Color::LightYellow);
let default_style = Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Rgb(50, 50, 50));
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
.show_weekdays_header(header_style)
.default_style(default_style)
.show_month_header(Style::default())
}
fn example3<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
let header_style = Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Green);
let default_style = Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Rgb(50, 50, 50));
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
.show_surrounding(Style::default().add_modifier(Modifier::DIM))
.show_weekdays_header(header_style)
.default_style(default_style)
.show_month_header(Style::default())
}
fn example4<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
let header_style = Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Green);
let default_style = Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Rgb(50, 50, 50));
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
.show_weekdays_header(header_style)
.default_style(default_style)
}
fn example5<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
let header_style = Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Green);
let default_style = Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Rgb(50, 50, 50));
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
.show_month_header(header_style)
.default_style(default_style)
}
}

View File

@@ -1,18 +1,24 @@
#[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 tui::{
backend::TermionBackend,
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout, Rect},
style::Color,
style::{Color, Style},
symbols::Marker,
text::Span,
widgets::{
canvas::{Canvas, Map, MapResolution, Rectangle},
Block, Borders,
},
Terminal,
Frame, Terminal,
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
struct App {
@@ -24,6 +30,8 @@ struct App {
vy: f64,
dir_x: bool,
dir_y: bool,
tick_count: u64,
marker: Marker,
}
impl App {
@@ -43,10 +51,22 @@ impl App {
vy: 1.0,
dir_x: true,
dir_y: true,
tick_count: 0,
marker: Marker::Dot,
}
}
fn update(&mut self) {
fn on_tick(&mut self) {
self.tick_count += 1;
// only change marker every 4 ticks (1s) to avoid stroboscopic effect
if (self.tick_count % 4) == 0 {
self.marker = match self.marker {
Marker::Dot => Marker::Block,
Marker::Block => Marker::Bar,
Marker::Bar => Marker::Braille,
Marker::Braille => Marker::Dot,
};
}
if self.ball.x < self.playground.left() as f64
|| self.ball.x + self.ball.width > self.playground.right() as f64
{
@@ -73,76 +93,105 @@ 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"))
.marker(app.marker)
.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"))
.marker(app.marker)
.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};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
symbols,
text::Span,
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType},
Terminal,
Frame, Terminal,
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
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,105 +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, Write},
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.
if event::poll(tick_rate - last_tick.elapsed()).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 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,
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
buffer::Buffer,
layout::Rect,
style::Style,
widgets::Widget,
Frame, Terminal,
};
use std::{error::Error, io};
#[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 ratatui::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>,
@@ -129,7 +247,7 @@ impl<'a> App<'a> {
App {
title,
should_quit: false,
tabs: TabsState::new(vec!["Tab0", "Tab1"]),
tabs: TabsState::new(vec!["Tab0", "Tab1", "Tab2"]),
show_chart: true,
progress: 0.0,
sparkline: Signal {

View File

@@ -0,0 +1,79 @@
use crate::{app::App, ui};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
Terminal,
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
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()? {
if key.kind == KeyEventKind::Press {
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;

83
examples/demo/termion.rs Normal file
View File

@@ -0,0 +1,83 @@
use crate::{app::App, ui};
use ratatui::{
backend::{Backend, TermionBackend},
Terminal,
};
use std::{error::Error, io, sync::mpsc, thread, time::Duration};
use termion::{
event::Key,
input::{MouseTerminal, TermRead},
raw::IntoRawMode,
screen::IntoAlternateScreen,
};
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
// setup terminal
let stdout = io::stdout()
.into_raw_mode()
.unwrap()
.into_alternate_screen()
.unwrap();
let stdout = MouseTerminal::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,5 +1,5 @@
use crate::demo::App;
use tui::{
use crate::app::App;
use ratatui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
@@ -7,8 +7,8 @@ use tui::{
text::{Span, Spans},
widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle},
widgets::{
Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, List, ListItem, Paragraph, Row,
Sparkline, Table, Tabs, Wrap,
Axis, BarChart, Block, Borders, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem,
Paragraph, Row, Sparkline, Table, Tabs, Wrap,
},
Frame,
};
@@ -31,6 +31,7 @@ pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
match app.tabs.index {
0 => draw_first_tab(f, app, chunks[1]),
1 => draw_second_tab(f, app, chunks[1]),
2 => draw_third_tab(f, app, chunks[1]),
_ => {}
};
}
@@ -42,8 +43,8 @@ where
let chunks = Layout::default()
.constraints(
[
Constraint::Length(7),
Constraint::Min(7),
Constraint::Length(9),
Constraint::Min(8),
Constraint::Length(7),
]
.as_ref(),
@@ -59,7 +60,14 @@ where
B: Backend,
{
let chunks = Layout::default()
.constraints([Constraint::Length(2), Constraint::Length(3)].as_ref())
.constraints(
[
Constraint::Length(2),
Constraint::Length(3),
Constraint::Length(1),
]
.as_ref(),
)
.margin(1)
.split(area);
let block = Block::default().borders(Borders::ALL).title("Graphs");
@@ -88,6 +96,17 @@ where
symbols::bar::THREE_LEVELS
});
f.render_widget(sparkline, chunks[1]);
let line_gauge = LineGauge::default()
.block(Block::default().title("LineGauge:"))
.gauge_style(Style::default().fg(Color::Magenta))
.line_set(if app.enhanced_graphics {
symbols::line::THICK
} else {
symbols::line::NORMAL
})
.ratio(app.progress);
f.render_widget(line_gauge, chunks[2]);
}
fn draw_charts<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
@@ -290,18 +309,21 @@ where
let failure_style = Style::default()
.fg(Color::Red)
.add_modifier(Modifier::RAPID_BLINK | Modifier::CROSSED_OUT);
let header = ["Server", "Location", "Status"];
let rows = app.servers.iter().map(|s| {
let style = if s.status == "Up" {
up_style
} else {
failure_style
};
Row::StyledData(vec![s.name, s.location, s.status].into_iter(), style)
Row::new(vec![s.name, s.location, s.status]).style(style)
});
let table = Table::new(header.iter(), rows)
let table = Table::new(rows)
.header(
Row::new(vec!["Server", "Location", "Status"])
.style(Style::default().fg(Color::Yellow))
.bottom_margin(1),
)
.block(Block::default().title("Servers").borders(Borders::ALL))
.header_style(Style::default().fg(Color::Yellow))
.widths(&[
Constraint::Length(15),
Constraint::Length(15),
@@ -341,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 {
@@ -353,3 +379,51 @@ where
.y_bounds([-90.0, 90.0]);
f.render_widget(map, chunks[1]);
}
fn draw_third_tab<B>(f: &mut Frame<B>, _app: &mut App, area: Rect)
where
B: Backend,
{
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
.split(area);
let colors = [
Color::Reset,
Color::Black,
Color::Red,
Color::Green,
Color::Yellow,
Color::Blue,
Color::Magenta,
Color::Cyan,
Color::Gray,
Color::DarkGray,
Color::LightRed,
Color::LightGreen,
Color::LightYellow,
Color::LightBlue,
Color::LightMagenta,
Color::LightCyan,
Color::White,
];
let items: Vec<Row> = colors
.iter()
.map(|c| {
let cells = vec![
Cell::from(Span::raw(format!("{:?}: ", c))),
Cell::from(Span::styled("Foreground", Style::default().fg(*c))),
Cell::from(Span::styled("Background", Style::default().bg(*c))),
];
Row::new(cells)
})
.collect();
let table = Table::new(items)
.block(Block::default().title("Colors").borders(Borders::ALL))
.widths(&[
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
]);
f.render_widget(table, chunks[0]);
}

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 tui::{
backend::TermionBackend,
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::Span,
widgets::{Block, Borders, Gauge},
Terminal,
Frame, Terminal,
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
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,77 +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);
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]);
}

292
examples/inline.rs Normal file
View File

@@ -0,0 +1,292 @@
use rand::distributions::{Distribution, Uniform};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
text::{Span, Spans},
widgets::{Block, Gauge, LineGauge, List, ListItem, Paragraph, Widget},
Frame, Terminal, TerminalOptions, Viewport,
};
use std::{
collections::{BTreeMap, VecDeque},
error::Error,
io,
sync::mpsc,
thread,
time::{Duration, Instant},
};
const NUM_DOWNLOADS: usize = 10;
type DownloadId = usize;
type WorkerId = usize;
enum Event {
Input(crossterm::event::KeyEvent),
Tick,
Resize,
DownloadUpdate(WorkerId, DownloadId, f64),
DownloadDone(WorkerId, DownloadId),
}
struct Downloads {
pending: VecDeque<Download>,
in_progress: BTreeMap<WorkerId, DownloadInProgress>,
}
impl Downloads {
fn next(&mut self, worker_id: WorkerId) -> Option<Download> {
match self.pending.pop_front() {
Some(d) => {
self.in_progress.insert(
worker_id,
DownloadInProgress {
id: d.id,
started_at: Instant::now(),
progress: 0.0,
},
);
Some(d)
}
None => None,
}
}
}
struct DownloadInProgress {
id: DownloadId,
started_at: Instant,
progress: f64,
}
struct Download {
id: DownloadId,
size: usize,
}
struct Worker {
id: WorkerId,
tx: mpsc::Sender<Download>,
}
fn main() -> Result<(), Box<dyn Error>> {
crossterm::terminal::enable_raw_mode()?;
let stdout = io::stdout();
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(8),
},
)?;
let (tx, rx) = mpsc::channel();
input_handling(tx.clone());
let workers = workers(tx);
let mut downloads = downloads();
for w in &workers {
let d = downloads.next(w.id).unwrap();
w.tx.send(d).unwrap();
}
run_app(&mut terminal, workers, downloads, rx)?;
crossterm::terminal::disable_raw_mode()?;
terminal.clear()?;
Ok(())
}
fn input_handling(tx: mpsc::Sender<Event>) {
let tick_rate = Duration::from_millis(200);
thread::spawn(move || {
let mut last_tick = Instant::now();
loop {
// poll for tick rate duration, if no events, sent tick event.
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout).unwrap() {
match crossterm::event::read().unwrap() {
crossterm::event::Event::Key(key) => tx.send(Event::Input(key)).unwrap(),
crossterm::event::Event::Resize(_, _) => tx.send(Event::Resize).unwrap(),
_ => {}
};
}
if last_tick.elapsed() >= tick_rate {
tx.send(Event::Tick).unwrap();
last_tick = Instant::now();
}
}
});
}
fn workers(tx: mpsc::Sender<Event>) -> Vec<Worker> {
(0..4)
.map(|id| {
let (worker_tx, worker_rx) = mpsc::channel::<Download>();
let tx = tx.clone();
thread::spawn(move || {
while let Ok(download) = worker_rx.recv() {
let mut remaining = download.size;
while remaining > 0 {
let wait = (remaining as u64).min(10);
thread::sleep(Duration::from_millis(wait * 10));
remaining = remaining.saturating_sub(10);
let progress = (download.size - remaining) * 100 / download.size;
tx.send(Event::DownloadUpdate(id, download.id, progress as f64))
.unwrap();
}
tx.send(Event::DownloadDone(id, download.id)).unwrap();
}
});
Worker { id, tx: worker_tx }
})
.collect()
}
fn downloads() -> Downloads {
let distribution = Uniform::new(0, 1000);
let mut rng = rand::thread_rng();
let pending = (0..NUM_DOWNLOADS)
.map(|id| {
let size = distribution.sample(&mut rng);
Download { id, size }
})
.collect();
Downloads {
pending,
in_progress: BTreeMap::new(),
}
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
workers: Vec<Worker>,
mut downloads: Downloads,
rx: mpsc::Receiver<Event>,
) -> Result<(), Box<dyn Error>> {
let mut redraw = true;
loop {
if redraw {
terminal.draw(|f| ui(f, &downloads))?;
}
redraw = true;
match rx.recv()? {
Event::Input(event) => {
if event.code == crossterm::event::KeyCode::Char('q') {
break;
}
}
Event::Resize => {
terminal.autoresize()?;
}
Event::Tick => {}
Event::DownloadUpdate(worker_id, _download_id, progress) => {
let download = downloads.in_progress.get_mut(&worker_id).unwrap();
download.progress = progress;
redraw = false
}
Event::DownloadDone(worker_id, download_id) => {
let download = downloads.in_progress.remove(&worker_id).unwrap();
terminal.insert_before(1, |buf| {
Paragraph::new(Spans::from(vec![
Span::from("Finished "),
Span::styled(
format!("download {}", download_id),
Style::default().add_modifier(Modifier::BOLD),
),
Span::from(format!(
" in {}ms",
download.started_at.elapsed().as_millis()
)),
]))
.render(buf.area, buf);
})?;
match downloads.next(worker_id) {
Some(d) => workers[worker_id].tx.send(d).unwrap(),
None => {
if downloads.in_progress.is_empty() {
terminal.insert_before(1, |buf| {
Paragraph::new("Done !").render(buf.area, buf);
})?;
break;
}
}
};
}
};
}
Ok(())
}
fn ui<B: Backend>(f: &mut Frame<B>, downloads: &Downloads) {
let size = f.size();
let block = Block::default()
.title("Progress")
.title_alignment(Alignment::Center);
f.render_widget(block, size);
let chunks = Layout::default()
.constraints(vec![Constraint::Length(2), Constraint::Length(4)])
.margin(1)
.split(size);
// total progress
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
let progress = LineGauge::default()
.gauge_style(Style::default().fg(Color::Blue))
.label(format!("{}/{}", done, NUM_DOWNLOADS))
.ratio(done as f64 / NUM_DOWNLOADS as f64);
f.render_widget(progress, chunks[0]);
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Percentage(20), Constraint::Percentage(80)])
.split(chunks[1]);
// in progress downloads
let items: Vec<ListItem> = downloads
.in_progress
.values()
.map(|download| {
ListItem::new(Spans::from(vec![
Span::raw(symbols::DOT),
Span::styled(
format!(" download {:>2}", download.id),
Style::default()
.fg(Color::LightGreen)
.add_modifier(Modifier::BOLD),
),
Span::raw(format!(
" ({}ms)",
download.started_at.elapsed().as_millis()
)),
]))
})
.collect();
let list = List::new(items);
f.render_widget(list, chunks[0]);
for (i, (_, download)) in downloads.in_progress.iter().enumerate() {
let gauge = Gauge::default()
.gauge_style(Style::default().fg(Color::Yellow))
.ratio(download.progress / 100.0);
if chunks[1].top().saturating_add(i as u16) > size.bottom() {
continue;
}
f.render_widget(
gauge,
Rect {
x: chunks[1].left(),
y: chunks[1].top().saturating_add(i as u16),
width: chunks[1].width,
height: 1,
},
);
}
}

View File

@@ -1,52 +1,70 @@
#[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,
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
widgets::{Block, Borders},
Terminal,
Frame, Terminal,
};
use std::{error::Error, io};
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,74 @@
#[allow(dead_code)]
mod util;
use crate::util::{
event::{Event, Events},
StatefulList,
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
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,
use ratatui::{
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,
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
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.
///
/// Check the event handling at the bottom to see how to change the state on incoming events.
/// Check the drawing logic for items on how to specify the highlighting style for selected items.
struct App<'a> {
items: StatefulList<(&'a str, usize)>,
events: Vec<(&'a str, &'a str)>,
@@ -82,112 +135,154 @@ impl<'a> App<'a> {
}
}
fn advance(&mut self) {
let event = self.events.pop().unwrap();
self.events.insert(0, event);
/// Rotate through the event list.
/// This only exists to simulate some kind of "progress"
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);
// 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 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();
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(">> ");
f.render_stateful_widget(items, chunks[0], &mut app.items.state);
let events: Vec<ListItem> = app
.events
.iter()
.map(|&(evt, level)| {
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(),
};
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),
),
]);
let log = Spans::from(vec![Span::raw(evt)]);
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]);
})?;
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()? {
if key.kind == KeyEventKind::Press {
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 ratatui::backend::{Backend, CrosstermBackend};
use ratatui::layout::Alignment;
use ratatui::text::Spans;
use ratatui::widgets::{Block, Borders, Paragraph};
use ratatui::{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 ratatui"),
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,173 @@
#[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,
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans},
widgets::{Block, Borders, Paragraph, Wrap},
Terminal,
Frame, Terminal,
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
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().fg(Color::Black));
f.render_widget(block, size);
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(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().fg(Color::Gray))
.title(Span::styled(
title,
Style::default().add_modifier(Modifier::BOLD),
))
};
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Default alignment (Left), no wrap"));
f.render_widget(paragraph, chunks[0]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Default alignment (Left), with wrap"))
.wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[1]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Right alignment, with wrap"))
.alignment(Alignment::Right)
.wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[2]);
let paragraph = Paragraph::new(text)
.style(Style::default().fg(Color::Gray))
.block(create_block("Center alignment, with wrap, with scroll"))
.alignment(Alignment::Center)
.wrap(Wrap { trim: true })
.scroll((app.scroll, 0));
f.render_widget(paragraph, chunks[3]);
}

View File

@@ -1,20 +1,108 @@
#[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,
use ratatui::{
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,
};
use std::{error::Error, io};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
/// helper function to create a centered rect using up
/// certain percentage of the available rect `r`
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()? {
if key.kind == KeyEventKind::Press {
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 +128,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,19 +1,46 @@
#[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 std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
use rand::{
distributions::{Distribution, Uniform},
rngs::ThreadRng,
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Style},
widgets::{Block, Borders, Sparkline},
Terminal,
Frame, Terminal,
};
use std::{
error::Error,
io,
time::{Duration, Instant},
};
#[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,
@@ -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 std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Row, Table, TableState},
Terminal,
widgets::{Block, Borders, Cell, Row, Table, TableState},
Frame, Terminal,
};
use std::{error::Error, io};
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"],
@@ -27,7 +27,7 @@ impl<'a> StatefulTable<'a> {
vec!["Row31", "Row32", "Row33"],
vec!["Row41", "Row42", "Row43"],
vec!["Row51", "Row52", "Row53"],
vec!["Row61", "Row62", "Row63"],
vec!["Row61", "Row62\nTest", "Row63"],
vec!["Row71", "Row72", "Row73"],
vec!["Row81", "Row82", "Row83"],
vec!["Row91", "Row92", "Row93"],
@@ -74,61 +74,84 @@ 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()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD);
let normal_style = Style::default().fg(Color::White);
let header = ["Header1", "Header2", "Header3"];
let rows = table
.items
.iter()
.map(|i| Row::StyledData(i.iter(), normal_style));
let t = Table::new(header.iter(), rows)
.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()? {
if key.kind == KeyEventKind::Press {
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,126 @@
#[allow(dead_code)]
mod util;
use crate::util::{
event::{Event, Events},
TabsState,
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
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,
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans},
widgets::{Block, Borders, Tabs},
Terminal,
Frame, Terminal,
};
use std::{error::Error, io};
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;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Right => app.next(),
KeyCode::Left => app.previous(),
_ => {}
}
Key::Right => app.tabs.next(),
Key::Left => app.tabs.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(())
}

View File

@@ -9,21 +9,20 @@
/// * 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 std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
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 std::{error::Error, io};
use unicode_width::UnicodeWidthStr;
enum InputMode {
@@ -52,130 +51,143 @@ 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 if key.kind == KeyEventKind::Press => 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 ratatui 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 = 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,5 +1,5 @@
use crate::{
backend::Backend,
backend::{Backend, ClearType},
buffer::Cell,
layout::Rect,
style::{Color, Modifier},
@@ -11,12 +11,9 @@ use crossterm::{
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
SetForegroundColor,
},
terminal::{self, Clear, ClearType},
};
use std::{
fmt,
io::{self, Write},
terminal::{self, Clear},
};
use std::io::{self, Write};
pub struct CrosstermBackend<W: Write> {
buffer: W,
@@ -52,9 +49,6 @@ where
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
use fmt::Write;
let mut string = String::with_capacity(content.size_hint().0 * 3);
let mut fg = Color::Reset;
let mut bg = Color::Reset;
let mut modifier = Modifier::empty();
@@ -62,7 +56,7 @@ where
for (x, y, cell) in content {
// Move the cursor if the previous location was not (x - 1, y)
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
map_error(queue!(string, MoveTo(x, y)))?;
map_error(queue!(self.buffer, MoveTo(x, y)))?;
}
last_pos = Some((x, y));
if cell.modifier != modifier {
@@ -70,26 +64,25 @@ where
from: modifier,
to: cell.modifier,
};
diff.queue(&mut string)?;
diff.queue(&mut self.buffer)?;
modifier = cell.modifier;
}
if cell.fg != fg {
let color = CColor::from(cell.fg);
map_error(queue!(string, SetForegroundColor(color)))?;
map_error(queue!(self.buffer, SetForegroundColor(color)))?;
fg = cell.fg;
}
if cell.bg != bg {
let color = CColor::from(cell.bg);
map_error(queue!(string, SetBackgroundColor(color)))?;
map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
bg = cell.bg;
}
string.push_str(&cell.symbol);
map_error(queue!(self.buffer, Print(&cell.symbol)))?;
}
map_error(queue!(
self.buffer,
Print(string),
SetForegroundColor(CColor::Reset),
SetBackgroundColor(CColor::Reset),
SetAttribute(CAttribute::Reset)
@@ -114,7 +107,27 @@ where
}
fn clear(&mut self) -> io::Result<()> {
map_error(execute!(self.buffer, Clear(ClearType::All)))
self.clear_region(ClearType::All)
}
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
map_error(execute!(
self.buffer,
Clear(match clear_type {
ClearType::All => crossterm::terminal::ClearType::All,
ClearType::AfterCursor => crossterm::terminal::ClearType::FromCursorDown,
ClearType::BeforeCursor => crossterm::terminal::ClearType::FromCursorUp,
ClearType::CurrentLine => crossterm::terminal::ClearType::CurrentLine,
ClearType::UntilNewLine => crossterm::terminal::ClearType::UntilNewLine,
})
))
}
fn append_lines(&mut self, n: u16) -> io::Result<()> {
for _ in 0..n {
map_error(queue!(self.buffer, Print("\n")))?;
}
self.buffer.flush()
}
fn size(&self) -> io::Result<Rect> {
@@ -165,11 +178,10 @@ struct ModifierDiff {
pub to: Modifier,
}
#[cfg(unix)]
impl ModifierDiff {
fn queue<W>(&self, mut w: W) -> io::Result<()>
where
W: fmt::Write,
W: io::Write,
{
//use crossterm::Attribute;
let removed = self.from - self.to;
@@ -227,28 +239,3 @@ impl ModifierDiff {
Ok(())
}
}
#[cfg(windows)]
impl ModifierDiff {
fn queue<W>(&self, mut w: W) -> io::Result<()>
where
W: fmt::Write,
{
let removed = self.from - self.to;
if removed.contains(Modifier::BOLD) {
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
}
if removed.contains(Modifier::UNDERLINED) {
map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
}
let added = self.to - self.from;
if added.contains(Modifier::BOLD) {
map_error(queue!(w, SetAttribute(CAttribute::Bold)))?;
}
if added.contains(Modifier::UNDERLINED) {
map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
}
Ok(())
}
}

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,23 +13,51 @@ mod crossterm;
#[cfg(feature = "crossterm")]
pub use self::crossterm::CrosstermBackend;
#[cfg(feature = "curses")]
mod curses;
#[cfg(feature = "curses")]
pub use self::curses::CursesBackend;
mod test;
pub use self::test::TestBackend;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ClearType {
All,
AfterCursor,
BeforeCursor,
CurrentLine,
UntilNewLine,
}
pub trait Backend {
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
where
I: Iterator<Item = (u16, u16, &'a Cell)>;
/// Insert `n` line breaks to the terminal screen
fn append_lines(&mut self, n: u16) -> io::Result<()> {
// to get around the unused warning
let _n = n;
Ok(())
}
fn hide_cursor(&mut self) -> Result<(), io::Error>;
fn show_cursor(&mut self) -> Result<(), io::Error>;
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error>;
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error>;
/// Clears the whole terminal screen
fn clear(&mut self) -> Result<(), io::Error>;
/// Clears a specific region of the terminal specified by the [`ClearType`] parameter
fn clear_region(&mut self, clear_type: ClearType) -> Result<(), io::Error> {
match clear_type {
ClearType::All => self.clear(),
ClearType::AfterCursor
| ClearType::BeforeCursor
| ClearType::CurrentLine
| ClearType::UntilNewLine => Err(io::Error::new(
io::ErrorKind::Other,
format!("clear_type [{clear_type:?}] not supported with this backend"),
)),
}
}
fn size(&self) -> Result<Rect, io::Error>;
fn flush(&mut self) -> Result<(), io::Error>;
}

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

@@ -1,5 +1,5 @@
use super::Backend;
use crate::{
backend::{Backend, ClearType},
buffer::Cell,
layout::Rect,
style::{Color, Modifier},
@@ -42,10 +42,25 @@ impl<W> Backend for TermionBackend<W>
where
W: Write,
{
/// Clears the entire screen and move the cursor to the top left of the screen
fn clear(&mut self) -> io::Result<()> {
write!(self.stdout, "{}", termion::clear::All)?;
write!(self.stdout, "{}", termion::cursor::Goto(1, 1))?;
self.clear_region(ClearType::All)
}
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
match clear_type {
ClearType::All => write!(self.stdout, "{}", termion::clear::All)?,
ClearType::AfterCursor => write!(self.stdout, "{}", termion::clear::AfterCursor)?,
ClearType::BeforeCursor => write!(self.stdout, "{}", termion::clear::BeforeCursor)?,
ClearType::CurrentLine => write!(self.stdout, "{}", termion::clear::CurrentLine)?,
ClearType::UntilNewLine => write!(self.stdout, "{}", termion::clear::UntilNewline)?,
};
self.stdout.flush()
}
fn append_lines(&mut self, n: u16) -> io::Result<()> {
for _ in 0..n {
writeln!(self.stdout)?;
}
self.stdout.flush()
}

View File

@@ -3,7 +3,10 @@ use crate::{
buffer::{Buffer, Cell},
layout::Rect,
};
use std::{fmt::Write, io};
use std::{
fmt::{Display, Write},
io,
};
use unicode_width::UnicodeWidthStr;
/// A backend used for the integration tests.
@@ -40,7 +43,7 @@ fn buffer_view(buffer: &Buffer) -> String {
)
.unwrap();
}
view.push_str("\n");
view.push('\n');
}
view
}
@@ -60,6 +63,12 @@ impl TestBackend {
&self.buffer
}
pub fn resize(&mut self, width: u16, height: u16) {
self.buffer.resize(Rect::new(0, 0, width, height));
self.width = width;
self.height = height;
}
pub fn assert_buffer(&self, expected: &Buffer) {
assert_eq!(expected.area, self.buffer.area);
let diff = expected.diff(&self.buffer);
@@ -68,20 +77,20 @@ impl TestBackend {
}
let mut debug_info = String::from("Buffers are not equal");
debug_info.push_str("\n");
debug_info.push('\n');
debug_info.push_str("Expected:");
debug_info.push_str("\n");
debug_info.push('\n');
let expected_view = buffer_view(expected);
debug_info.push_str(&expected_view);
debug_info.push_str("\n");
debug_info.push('\n');
debug_info.push_str("Got:");
debug_info.push_str("\n");
debug_info.push('\n');
let view = buffer_view(&self.buffer);
debug_info.push_str(&view);
debug_info.push_str("\n");
debug_info.push('\n');
debug_info.push_str("Diff:");
debug_info.push_str("\n");
debug_info.push('\n');
let nice_diff = diff
.iter()
.enumerate()
@@ -95,7 +104,13 @@ impl TestBackend {
.collect::<Vec<String>>()
.join("\n");
debug_info.push_str(&nice_diff);
panic!(debug_info);
panic!("{}", debug_info);
}
}
impl Display for TestBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", buffer_view(&self.buffer))
}
}
@@ -131,6 +146,7 @@ impl Backend for TestBackend {
}
fn clear(&mut self) -> Result<(), io::Error> {
self.buffer.reset();
Ok(())
}

View File

@@ -3,12 +3,15 @@ use crate::{
style::{Color, Modifier, Style},
text::{Span, Spans},
};
use std::cmp::min;
use std::{
cmp::min,
fmt::{Debug, Formatter, Result},
};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
/// A buffer cell
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cell {
pub symbol: String,
pub fg: Color,
@@ -51,6 +54,13 @@ impl Cell {
self
}
pub fn style(&self) -> Style {
Style::default()
.fg(self.fg)
.bg(self.bg)
.add_modifier(self.modifier)
}
pub fn reset(&mut self) {
self.symbol.clear();
self.symbol.push(' ');
@@ -81,9 +91,9 @@ impl Default for Cell {
/// # Examples:
///
/// ```
/// use tui::buffer::{Buffer, Cell};
/// use tui::layout::Rect;
/// use tui::style::{Color, Style, Modifier};
/// use ratatui::buffer::{Buffer, Cell};
/// use ratatui::layout::Rect;
/// use ratatui::style::{Color, Style, Modifier};
///
/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
/// buf.get_mut(0, 2).set_symbol("x");
@@ -98,7 +108,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(Clone, PartialEq, Eq, Default)]
pub struct Buffer {
/// The area represented by this buffer
pub area: Rect,
@@ -107,15 +117,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 {
@@ -139,9 +140,11 @@ impl Buffer {
S: AsRef<str>,
{
let height = lines.len() as u16;
let width = lines.iter().fold(0, |acc, item| {
std::cmp::max(acc, item.as_ref().width() as u16)
});
let width = lines
.iter()
.map(|i| i.as_ref().width() as u16)
.max()
.unwrap_or_default();
let mut buffer = Buffer::empty(Rect {
x: 0,
y: 0,
@@ -176,15 +179,15 @@ impl Buffer {
&mut self.content[i]
}
/// Returns the index in the Vec<Cell> for the given global (x, y) coordinates.
/// Returns the index in the `Vec<Cell>` for the given global (x, y) coordinates.
///
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
///
/// # Examples
///
/// ```
/// # use tui::buffer::Buffer;
/// # use tui::layout::Rect;
/// # use ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
/// // Global coordinates to the top corner of this buffer's area
@@ -196,8 +199,8 @@ impl Buffer {
/// Panics when given an coordinate that is outside of this Buffer's area.
///
/// ```should_panic
/// # use tui::buffer::Buffer;
/// # use tui::layout::Rect;
/// # use ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
/// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
@@ -225,8 +228,8 @@ impl Buffer {
/// # Examples
///
/// ```
/// # use tui::buffer::Buffer;
/// # use tui::layout::Rect;
/// # use ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
/// assert_eq!(buffer.pos_of(0), (200, 100));
@@ -238,8 +241,8 @@ impl Buffer {
/// Panics when given an index that is outside the Buffer's content.
///
/// ```should_panic
/// # use tui::buffer::Buffer;
/// # use tui::layout::Rect;
/// # use ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
/// let buffer = Buffer::empty(rect);
/// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
@@ -289,7 +292,7 @@ impl Buffer {
continue;
}
// `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
// change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32.
// change dimensions to usize or u32 and someone resizes the terminal to 1x2^32.
if width > max_offset.saturating_sub(x_offset) {
break;
}
@@ -306,7 +309,7 @@ impl Buffer {
(x_offset as u16, y)
}
pub fn set_spans<'a>(&mut self, x: u16, y: u16, spans: &Spans<'a>, width: u16) -> (u16, u16) {
pub fn set_spans(&mut self, x: u16, y: u16, spans: &Spans<'_>, width: u16) -> (u16, u16) {
let mut remaining_width = width;
let mut x = x;
for span in &spans.0 {
@@ -327,7 +330,7 @@ impl Buffer {
(x, y)
}
pub fn set_span<'a>(&mut self, x: u16, y: u16, span: &Span<'a>, width: u16) -> (u16, u16) {
pub fn set_span(&mut self, x: u16, y: u16, span: &Span<'_>, width: u16) -> (u16, u16) {
self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style)
}
@@ -431,18 +434,16 @@ impl Buffer {
pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> {
let previous_buffer = &self.content;
let next_buffer = &other.content;
let width = self.area.width;
let mut updates: Vec<(u16, u16, &Cell)> = vec![];
// Cells invalidated by drawing/replacing preceeding multi-width characters:
// Cells invalidated by drawing/replacing preceding multi-width characters:
let mut invalidated: usize = 0;
// Cells from the current buffer to skip due to preceeding multi-width characters taking their
// Cells from the current buffer to skip due to preceding multi-width characters taking their
// place (the skipped cells should be blank anyway):
let mut to_skip: usize = 0;
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
if (current != previous || invalidated > 0) && to_skip == 0 {
let x = i as u16 % width;
let y = i as u16 / width;
let (x, y) = self.pos_of(i);
updates.push((x, y, &next_buffer[i]));
}
@@ -455,6 +456,113 @@ impl Buffer {
}
}
/// Assert that two buffers are equal by comparing their areas and content.
///
/// On panic, displays the areas or the content and a diff of the contents.
#[macro_export]
macro_rules! assert_buffer_eq {
($actual_expr:expr, $expected_expr:expr) => {
match (&$actual_expr, &$expected_expr) {
(actual, expected) => {
if actual.area != expected.area {
panic!(
indoc::indoc!(
"
buffer areas not equal
expected: {:?}
actual: {:?}"
),
expected, actual
);
}
let diff = expected.diff(&actual);
if !diff.is_empty() {
let nice_diff = diff
.iter()
.enumerate()
.map(|(i, (x, y, cell))| {
let expected_cell = expected.get(*x, *y);
format!(
"{}: at ({}, {})\n expected: {:?}\n actual: {:?}",
i, x, y, expected_cell, cell
)
})
.collect::<Vec<String>>()
.join("\n");
panic!(
indoc::indoc!(
"
buffer contents not equal
expected: {:?}
actual: {:?}
diff:
{}"
),
expected, actual, nice_diff
);
}
// shouldn't get here, but this guards against future behavior
// that changes equality but not area or content
assert_eq!(actual, expected, "buffers not equal");
}
}
};
}
impl Debug for Buffer {
/// Writes a debug representation of the buffer to the given formatter.
///
/// The format is like a pretty printed struct, with the following fields:
/// area: displayed as Rect { x: 1, y: 2, width: 3, height: 4 }
/// content: displayed as a list of strings representing the content of the
/// buffer
/// styles: displayed as a list of
/// { x: 1, y: 2, fg: Color::Red, bg: Color::Blue, modifier: Modifier::BOLD }
/// only showing a value when there is a change in style.
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.write_fmt(format_args!(
"Buffer {{\n area: {:?},\n content: [\n",
&self.area
))?;
let mut last_style = None;
let mut styles = vec![];
for (y, line) in self.content.chunks(self.area.width as usize).enumerate() {
let mut overwritten = vec![];
let mut skip: usize = 0;
f.write_str(" \"")?;
for (x, c) in line.iter().enumerate() {
if skip == 0 {
f.write_str(&c.symbol)?;
} else {
overwritten.push((x, &c.symbol))
}
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
let style = (c.fg, c.bg, c.modifier);
if last_style != Some(style) {
last_style = Some(style);
styles.push((x, y, c.fg, c.bg, c.modifier));
}
}
if !overwritten.is_empty() {
f.write_fmt(format_args!(
"// hidden by multi-width symbols: {:?}",
overwritten
))?;
}
f.write_str("\",\n")?;
}
f.write_str(" ],\n styles: [\n")?;
for s in styles {
f.write_fmt(format_args!(
" x: {}, y: {}, fg: {:?}, bg: {:?}, modifier: {:?},\n",
s.0, s.1, s.2, s.3, s.4
))?;
}
f.write_str(" ]\n}")?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -465,6 +573,62 @@ mod tests {
cell
}
#[test]
fn it_implements_debug() {
let mut buf = Buffer::empty(Rect::new(0, 0, 12, 2));
buf.set_string(0, 0, "Hello World!", Style::default());
buf.set_string(
0,
1,
"G'day World!",
Style::default()
.fg(Color::Green)
.bg(Color::Yellow)
.add_modifier(Modifier::BOLD),
);
assert_eq!(
format!("{:?}", buf),
indoc::indoc!(
"
Buffer {
area: Rect { x: 0, y: 0, width: 12, height: 2 },
content: [
\"Hello World!\",
\"G'day World!\",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, modifier: (empty),
x: 0, y: 1, fg: Green, bg: Yellow, modifier: BOLD,
]
}"
)
);
}
#[test]
fn assert_buffer_eq_does_not_panic_on_equal_buffers() {
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
let other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
assert_buffer_eq!(buffer, other_buffer);
}
#[should_panic]
#[test]
fn assert_buffer_eq_panics_on_unequal_area() {
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
let other_buffer = Buffer::empty(Rect::new(0, 0, 6, 1));
assert_buffer_eq!(buffer, other_buffer);
}
#[should_panic]
#[test]
fn assert_buffer_eq_panics_on_unequal_style() {
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
let mut other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
other_buffer.set_string(0, 0, " ", Style::default().fg(Color::Red));
assert_buffer_eq!(buffer, other_buffer);
}
#[test]
fn it_translates_to_and_from_coordinates() {
let rect = Rect::new(200, 100, 50, 80);
@@ -506,21 +670,38 @@ mod tests {
// Zero-width
buffer.set_stringn(0, 0, "aaa", 0, Style::default());
assert_eq!(buffer, Buffer::with_lines(vec![" "]));
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" "]));
buffer.set_string(0, 0, "aaa", Style::default());
assert_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
// Width limit:
buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
assert_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
buffer.set_string(0, 0, "12345", Style::default());
assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
// Width truncation:
buffer.set_string(0, 0, "123456", Style::default());
assert_eq!(buffer, Buffer::with_lines(vec!["12345"]));
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
// multi-line
buffer = Buffer::empty(Rect::new(0, 0, 5, 2));
buffer.set_string(0, 0, "12345", Style::default());
buffer.set_string(0, 1, "67890", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345", "67890"]));
}
#[test]
fn buffer_set_string_multi_width_overwrite() {
let area = Rect::new(0, 0, 5, 1);
let mut buffer = Buffer::empty(area);
// multi-width overwrite
buffer.set_string(0, 0, "aaaaa", Style::default());
buffer.set_string(0, 0, "称号", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["称号a"]));
}
#[test]
@@ -531,12 +712,12 @@ mod tests {
// Leading grapheme with zero width
let s = "\u{1}a";
buffer.set_stringn(0, 0, s, 1, Style::default());
assert_eq!(buffer, Buffer::with_lines(vec!["a"]));
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
// Trailing grapheme with zero with
let s = "a\u{1}";
buffer.set_stringn(0, 0, s, 1, Style::default());
assert_eq!(buffer, Buffer::with_lines(vec!["a"]));
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
}
#[test]
@@ -544,11 +725,11 @@ mod tests {
let area = Rect::new(0, 0, 5, 1);
let mut buffer = Buffer::empty(area);
buffer.set_string(0, 0, "コン", Style::default());
assert_eq!(buffer, Buffer::with_lines(vec!["コン "]));
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
// Only 1 space left.
buffer.set_string(0, 0, "コンピ", Style::default());
assert_eq!(buffer, Buffer::with_lines(vec!["コン "]));
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
}
#[test]
@@ -673,7 +854,7 @@ mod tests {
Cell::default().set_symbol("2"),
);
one.merge(&two);
assert_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
assert_buffer_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
}
#[test]
@@ -697,7 +878,7 @@ mod tests {
Cell::default().set_symbol("2"),
);
one.merge(&two);
assert_eq!(
assert_buffer_eq!(
one,
Buffer::with_lines(vec!["22 ", "22 ", " 11", " 11"])
);
@@ -731,6 +912,6 @@ mod tests {
width: 4,
height: 4,
};
assert_eq!(one, merged);
assert_buffer_eq!(one, merged);
}
}

View File

@@ -1,8 +1,9 @@
use std::cell::RefCell;
use std::cmp::{max, min};
use std::collections::HashMap;
use std::rc::Rc;
use cassowary::strength::{REQUIRED, WEAK};
use cassowary::strength::{MEDIUM, REQUIRED, WEAK};
use cassowary::WeightedRelation::*;
use cassowary::{Constraint as CassowaryConstraint, Expression, Solver, Variable};
@@ -51,7 +52,7 @@ pub struct Margin {
pub horizontal: u16,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Alignment {
Left,
Center,
@@ -63,10 +64,14 @@ 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,
}
type Cache = HashMap<(Rect, Layout), Rc<[Rect]>>;
thread_local! {
static LAYOUT_CACHE: RefCell<HashMap<(Rect, Layout), Vec<Rect>>> = RefCell::new(HashMap::new());
static LAYOUT_CACHE: RefCell<Cache> = RefCell::new(HashMap::new());
}
impl Default for Layout {
@@ -78,6 +83,7 @@ impl Default for Layout {
vertical: 0,
},
constraints: Vec::new(),
expand_to_fill: true,
}
}
}
@@ -114,12 +120,17 @@ 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.
///
/// # Examples
/// ```
/// # use tui::layout::{Rect, Constraint, Direction, Layout};
/// # use ratatui::layout::{Rect, Constraint, Direction, Layout};
/// let chunks = Layout::default()
/// .direction(Direction::Vertical)
/// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref())
@@ -130,8 +141,8 @@ impl Layout {
/// height: 10,
/// });
/// assert_eq!(
/// chunks,
/// vec![
/// chunks[..],
/// [
/// Rect {
/// x: 2,
/// y: 2,
@@ -157,8 +168,8 @@ impl Layout {
/// height: 2,
/// });
/// assert_eq!(
/// chunks,
/// vec![
/// chunks[..],
/// [
/// Rect {
/// x: 0,
/// y: 0,
@@ -174,7 +185,7 @@ impl Layout {
/// ]
/// );
/// ```
pub fn split(&self, area: Rect) -> Vec<Rect> {
pub fn split(&self, area: Rect) -> Rc<[Rect]> {
// TODO: Maybe use a fixed size cache ?
LAYOUT_CACHE.with(|c| {
c.borrow_mut()
@@ -185,7 +196,7 @@ impl Layout {
}
}
fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
fn split(area: Rect, layout: &Layout) -> Rc<[Rect]> {
let mut solver = Solver::new();
let mut vars: HashMap<Variable, (usize, usize)> = HashMap::new();
let elements = layout
@@ -193,11 +204,13 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
.iter()
.map(|_| Element::new())
.collect::<Vec<Element>>();
let mut results = layout
let mut res = layout
.constraints
.iter()
.map(|_| Rect::default())
.collect::<Vec<Rect>>();
.collect::<Rc<[Rect]>>();
let mut results = Rc::get_mut(&mut res).expect("newly created Rc should have no shared refs");
let dest_area = area.inner(&layout.margin);
for (i, e) in elements.iter().enumerate() {
@@ -222,11 +235,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 => {
@@ -237,18 +252,28 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
ccs.push(elements[i].y | EQ(REQUIRED) | f64::from(dest_area.y));
ccs.push(elements[i].height | EQ(REQUIRED) | f64::from(dest_area.height));
ccs.push(match *size {
Constraint::Length(v) => elements[i].width | EQ(WEAK) | f64::from(v),
Constraint::Length(v) => elements[i].width | EQ(MEDIUM) | f64::from(v),
Constraint::Percentage(v) => {
elements[i].width | EQ(WEAK) | (f64::from(v * dest_area.width) / 100.0)
elements[i].width | EQ(MEDIUM) | (f64::from(v * dest_area.width) / 100.0)
}
Constraint::Ratio(n, d) => {
elements[i].width
| EQ(WEAK)
| EQ(MEDIUM)
| (f64::from(dest_area.width) * f64::from(n) / f64::from(d))
}
Constraint::Min(v) => elements[i].width | GE(WEAK) | f64::from(v),
Constraint::Max(v) => elements[i].width | LE(WEAK) | f64::from(v),
Constraint::Min(v) => elements[i].width | GE(MEDIUM) | f64::from(v),
Constraint::Max(v) => elements[i].width | LE(MEDIUM) | f64::from(v),
});
match *size {
Constraint::Min(v) => {
ccs.push(elements[i].width | EQ(WEAK) | f64::from(v));
}
Constraint::Max(v) => {
ccs.push(elements[i].width | EQ(WEAK) | f64::from(v));
}
_ => {}
}
}
}
Direction::Vertical => {
@@ -259,18 +284,28 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
ccs.push(elements[i].x | EQ(REQUIRED) | f64::from(dest_area.x));
ccs.push(elements[i].width | EQ(REQUIRED) | f64::from(dest_area.width));
ccs.push(match *size {
Constraint::Length(v) => elements[i].height | EQ(WEAK) | f64::from(v),
Constraint::Length(v) => elements[i].height | EQ(MEDIUM) | f64::from(v),
Constraint::Percentage(v) => {
elements[i].height | EQ(WEAK) | (f64::from(v * dest_area.height) / 100.0)
elements[i].height | EQ(MEDIUM) | (f64::from(v * dest_area.height) / 100.0)
}
Constraint::Ratio(n, d) => {
elements[i].height
| EQ(WEAK)
| EQ(MEDIUM)
| (f64::from(dest_area.height) * f64::from(n) / f64::from(d))
}
Constraint::Min(v) => elements[i].height | GE(WEAK) | f64::from(v),
Constraint::Max(v) => elements[i].height | LE(WEAK) | f64::from(v),
Constraint::Min(v) => elements[i].height | GE(MEDIUM) | f64::from(v),
Constraint::Max(v) => elements[i].height | LE(MEDIUM) | f64::from(v),
});
match *size {
Constraint::Min(v) => {
ccs.push(elements[i].height | EQ(WEAK) | f64::from(v));
}
Constraint::Max(v) => {
ccs.push(elements[i].height | EQ(WEAK) | f64::from(v));
}
_ => {}
}
}
}
}
@@ -299,18 +334,20 @@ 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;
}
}
}
}
results
res
}
/// A container used by the solver inside split
@@ -348,9 +385,9 @@ impl Element {
}
}
/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
/// A simple rectangle used in the computation of the layout and to give widgets a 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 +395,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.
@@ -401,7 +427,7 @@ impl Rect {
}
pub fn right(self) -> u16 {
self.x + self.width
self.x.saturating_add(self.width)
}
pub fn top(self) -> u16 {
@@ -409,7 +435,7 @@ impl Rect {
}
pub fn bottom(self) -> u16 {
self.y + self.height
self.y.saturating_add(self.height)
}
pub fn inner(self, margin: &Margin) -> Rect {

View File

@@ -1,61 +1,69 @@
//! [tui](https://github.com/fdehau/tui-rs) is a library used to build rich
//! [ratatui](https://github.com/tui-rs-revival/ratatui) is a library used to build rich
//! terminal users interfaces and dashboards.
//!
//! ![](https://raw.githubusercontent.com/fdehau/tui-rs/master/assets/demo.gif)
//! ![](https://raw.githubusercontent.com/tui-rs-revival/ratatui/master/assets/demo.gif)
//!
//! # Get started
//!
//! ## Adding `tui` as a dependency
//! ## Adding `ratatui` as a dependency
//!
//! Add the following to your `Cargo.toml`:
//! ```toml
//! [dependencies]
//! tui = "0.10"
//! termion = "1.5"
//! crossterm = "0.26"
//! ratatui = "0.20"
//! ```
//!
//! 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.17"
//! tui = { version = "0.10", default-features = false, features = ['crossterm'] }
//! termion = "1.5"
//! ratatui = { version = "0.20", default-features = false, features = ['termion'] }
//!
//! ```
//!
//! The same logic applies for all other available backends.
//!
//! ### Features
//!
//! Widgets which add dependencies are gated behind feature flags to prevent unused transitive
//! dependencies. The available features are:
//!
//! * `widget-calendar` - enables [`widgets::calendar`] and adds a dependency on the [time
//! crate](https://crates.io/crates/time).
//!
//! ## Creating a `Terminal`
//!
//! Every application using `tui` should start by instantiating a `Terminal`. It is a light
//! Every application using `ratatui` should start by instantiating a `Terminal`. It is a light
//! abstraction over available backends that provides basic functionalities such as clearing the
//! screen, hiding the cursor, etc.
//!
//! ```rust,no_run
//! use std::io;
//! use tui::Terminal;
//! use tui::backend::TermionBackend;
//! use termion::raw::IntoRawMode;
//! use ratatui::{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 ratatui::{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,30 +79,58 @@
//! 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 ratatui::{
//! backend::CrosstermBackend,
//! widgets::{Block, Borders},
//! Terminal
//! };
//! use crossterm::{
//! event::{self, DisableMouseCapture, EnableMouseCapture},
//! 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()
//! .title("Block")
//! .borders(Borders::ALL);
//! f.render_widget(block, size);
//! })
//! })?;
//!
//! // Start a thread to discard any input events. Without handling events, the
//! // stdin buffer will fill up, and be read into the shell when the program exits.
//! thread::spawn(|| loop {
//! event::read();
//! });
//!
//! thread::sleep(Duration::from_millis(5000));
//!
//! // restore terminal
//! disable_raw_mode()?;
//! execute!(
//! terminal.backend_mut(),
//! LeaveAlternateScreen,
//! DisableMouseCapture
//! )?;
//! terminal.show_cursor()?;
//!
//! Ok(())
//! }
//! ```
//!
@@ -105,38 +141,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]);
//! })
//! use ratatui::{
//! 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 bitflags::bitflags;
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Color {
Reset,
@@ -34,7 +34,7 @@ bitflags! {
/// ## Examples
///
/// ```rust
/// # use tui::style::Modifier;
/// # use ratatui::style::Modifier;
///
/// let m = Modifier::BOLD | Modifier::ITALIC;
/// ```
@@ -54,16 +54,68 @@ bitflags! {
/// Style let you control the main characteristics of the displayed elements.
///
/// ## Examples
///
/// ```rust
/// # use tui::style::{Color, Modifier, Style};
/// # use ratatui::style::{Color, Modifier, Style};
/// Style::default()
/// .fg(Color::Black)
/// .bg(Color::Green)
/// .add_modifier(Modifier::ITALIC | Modifier::BOLD);
/// ```
#[derive(Debug, Clone, Copy, PartialEq)]
///
/// It represents an incremental change. If you apply the styles S1, S2, S3 to a cell of the
/// terminal buffer, the style of this cell will be the result of the merge of S1, S2 and S3, not
/// just S3.
///
/// ```rust
/// # use ratatui::style::{Color, Modifier, Style};
/// # use ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// let styles = [
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
/// Style::default().bg(Color::Red),
/// Style::default().fg(Color::Yellow).remove_modifier(Modifier::ITALIC),
/// ];
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
/// for style in &styles {
/// buffer.get_mut(0, 0).set_style(*style);
/// }
/// assert_eq!(
/// Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Red),
/// add_modifier: Modifier::BOLD,
/// sub_modifier: Modifier::empty(),
/// },
/// buffer.get(0, 0).style(),
/// );
/// ```
///
/// The default implementation returns a `Style` that does not modify anything. If you wish to
/// reset all properties until that point use [`Style::reset`].
///
/// ```
/// # use ratatui::style::{Color, Modifier, Style};
/// # use ratatui::buffer::Buffer;
/// # use ratatui::layout::Rect;
/// let styles = [
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
/// Style::reset().fg(Color::Yellow),
/// ];
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
/// for style in &styles {
/// buffer.get_mut(0, 0).set_style(*style);
/// }
/// assert_eq!(
/// Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Reset),
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
/// buffer.get(0, 0).style(),
/// );
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Style {
pub fg: Option<Color>,
@@ -74,6 +126,12 @@ pub struct Style {
impl Default for Style {
fn default() -> Style {
Style::new()
}
}
impl Style {
pub const fn new() -> Style {
Style {
fg: None,
bg: None,
@@ -81,20 +139,28 @@ impl Default for Style {
sub_modifier: Modifier::empty(),
}
}
}
impl Style {
/// Returns a `Style` resetting all properties.
pub const fn reset() -> Style {
Style {
fg: Some(Color::Reset),
bg: Some(Color::Reset),
add_modifier: Modifier::empty(),
sub_modifier: Modifier::all(),
}
}
/// Changes the foreground color.
///
/// ## Examples
///
/// ```rust
/// # use tui::style::{Color, Style};
/// # use ratatui::style::{Color, Style};
/// let style = Style::default().fg(Color::Blue);
/// let diff = Style::default().fg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
/// ```
pub fn fg(mut self, color: Color) -> Style {
pub const fn fg(mut self, color: Color) -> Style {
self.fg = Some(color);
self
}
@@ -104,12 +170,12 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use tui::style::{Color, Style};
/// # use ratatui::style::{Color, Style};
/// let style = Style::default().bg(Color::Blue);
/// let diff = Style::default().bg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
/// ```
pub fn bg(mut self, color: Color) -> Style {
pub const fn bg(mut self, color: Color) -> Style {
self.bg = Some(color);
self
}
@@ -121,7 +187,7 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use tui::style::{Color, Modifier, Style};
/// # use ratatui::style::{Color, Modifier, Style};
/// let style = Style::default().add_modifier(Modifier::BOLD);
/// let diff = Style::default().add_modifier(Modifier::ITALIC);
/// let patched = style.patch(diff);
@@ -141,7 +207,7 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use tui::style::{Color, Modifier, Style};
/// # use ratatui::style::{Color, Modifier, Style};
/// let style = Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC);
/// let diff = Style::default().remove_modifier(Modifier::ITALIC);
/// let patched = style.patch(diff);
@@ -159,7 +225,7 @@ impl Style {
///
/// ## Examples
/// ```
/// # use tui::style::{Color, Modifier, Style};
/// # use ratatui::style::{Color, Modifier, Style};
/// let style_1 = Style::default().fg(Color::Yellow);
/// let style_2 = Style::default().bg(Color::Red);
/// let combined = style_1.patch(style_2);
@@ -216,4 +282,33 @@ mod tests {
}
}
}
#[test]
fn combine_individual_modifiers() {
use crate::{buffer::Buffer, layout::Rect};
let mods = vec![
Modifier::BOLD,
Modifier::DIM,
Modifier::ITALIC,
Modifier::UNDERLINED,
Modifier::SLOW_BLINK,
Modifier::RAPID_BLINK,
Modifier::REVERSED,
Modifier::HIDDEN,
Modifier::CROSSED_OUT,
];
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
for m in &mods {
buffer.get_mut(0, 0).set_style(Style::reset());
buffer
.get_mut(0, 0)
.set_style(Style::default().add_modifier(*m));
let style = buffer.get(0, 0).style();
assert!(style.add_modifier.contains(*m));
assert!(!style.sub_modifier.contains(*m));
}
}
}

View File

@@ -224,8 +224,12 @@ pub mod braille {
/// Marker to use when plotting data points
#[derive(Debug, Clone, Copy)]
pub enum Marker {
/// One point per cell
/// One point per cell in shape of dot
Dot,
/// One point per cell in shape of a block
Block,
/// One point per cell in the shape of a bar
Bar,
/// Up to 8 points per cell
Braille,
}

View File

@@ -1,5 +1,5 @@
use crate::{
backend::Backend,
backend::{Backend, ClearType},
buffer::Buffer,
layout::Rect,
widgets::{StatefulWidget, Widget},
@@ -7,31 +7,14 @@ use crate::{
use std::io;
#[derive(Debug, Clone, PartialEq)]
/// UNSTABLE
enum ResizeBehavior {
Fixed,
Auto,
pub enum Viewport {
Fullscreen,
Inline(u16),
Fixed(Rect),
}
#[derive(Debug, Clone, PartialEq)]
/// UNSTABLE
pub struct Viewport {
area: Rect,
resize_behavior: ResizeBehavior,
}
impl Viewport {
/// UNSTABLE
pub fn fixed(area: Rect) -> Viewport {
Viewport {
area,
resize_behavior: ResizeBehavior::Fixed,
}
}
}
#[derive(Debug, Clone, PartialEq)]
/// Options to pass to [`Terminal::draw_with_options`]
/// Options to pass to [`Terminal::with_options`]
pub struct TerminalOptions {
/// Viewport used to draw to the terminal
pub viewport: Viewport,
@@ -53,6 +36,12 @@ where
hidden_cursor: bool,
/// Viewport
viewport: Viewport,
viewport_area: Rect,
/// Last known size of the terminal. Used to detect if the internal buffers have to be resized.
last_known_size: Rect,
/// Last known position of the cursor. Used to find the new area when the viewport is inlined
/// and the terminal resized.
last_known_cursor_pos: (u16, u16),
}
/// Represents a consistent terminal interface for rendering.
@@ -73,23 +62,21 @@ impl<'a, B> Frame<'a, B>
where
B: Backend,
{
/// Terminal size, guaranteed not to change when rendering.
/// Frame size, guaranteed not to change when rendering.
pub fn size(&self) -> Rect {
self.terminal.viewport.area
self.terminal.viewport_area
}
/// Render a [`Widget`] to the current buffer using [`Widget::render`].
///
/// # Examples
///
/// ```rust,no_run
/// # use std::io;
/// # use tui::Terminal;
/// # use tui::backend::TermionBackend;
/// # use tui::layout::Rect;
/// # use tui::widgets::Block;
/// # let stdout = io::stdout();
/// # let backend = TermionBackend::new(stdout);
/// ```rust
/// # use ratatui::Terminal;
/// # use ratatui::backend::TestBackend;
/// # use ratatui::layout::Rect;
/// # use ratatui::widgets::Block;
/// # 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 +97,12 @@ where
///
/// # Examples
///
/// ```rust,no_run
/// # use std::io;
/// # use tui::Terminal;
/// # use tui::backend::TermionBackend;
/// # use tui::layout::Rect;
/// # use tui::widgets::{List, ListItem, ListState};
/// # let stdout = io::stdout();
/// # let backend = TermionBackend::new(stdout);
/// ```rust
/// # use ratatui::Terminal;
/// # use ratatui::backend::TestBackend;
/// # use ratatui::layout::Rect;
/// # use ratatui::widgets::{List, ListItem, ListState};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// let mut state = ListState::default();
/// state.select(Some(1));
@@ -148,6 +133,14 @@ where
}
}
/// CompletedFrame represents the state of the terminal after all changes performed in the last
/// [`Terminal::draw`] call have been applied. Therefore, it is only valid until the next call to
/// [`Terminal::draw`].
pub struct CompletedFrame<'a> {
pub buffer: &'a Buffer,
pub area: Rect,
}
impl<B> Drop for Terminal<B>
where
B: Backend,
@@ -169,29 +162,33 @@ where
/// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and
/// default colors for the foreground and the background
pub fn new(backend: B) -> io::Result<Terminal<B>> {
let size = backend.size()?;
Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport {
area: size,
resize_behavior: ResizeBehavior::Auto,
},
viewport: Viewport::Fullscreen,
},
)
}
/// UNSTABLE
pub fn with_options(backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
let size = match options.viewport {
Viewport::Fullscreen | Viewport::Inline(_) => backend.size()?,
Viewport::Fixed(area) => area,
};
let (viewport_area, cursor_pos) = match options.viewport {
Viewport::Fullscreen => (size, (0, 0)),
Viewport::Inline(height) => compute_inline_size(&mut backend, height, size, 0)?,
Viewport::Fixed(area) => (area, (area.left(), area.top())),
};
Ok(Terminal {
backend,
buffers: [
Buffer::empty(options.viewport.area),
Buffer::empty(options.viewport.area),
],
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
current: 0,
hidden_cursor: false,
viewport: options.viewport,
viewport_area,
last_known_size: size,
last_known_cursor_pos: cursor_pos,
})
}
@@ -221,25 +218,46 @@ where
let previous_buffer = &self.buffers[1 - self.current];
let current_buffer = &self.buffers[self.current];
let updates = previous_buffer.diff(current_buffer);
if let Some((col, row, _)) = updates.last() {
self.last_known_cursor_pos = (*col, *row);
}
self.backend.draw(updates.into_iter())
}
/// Updates the Terminal so that internal buffers match the requested size. Requested size will
/// be saved so the size can remain consistent when rendering.
/// This leads to a full clear of the screen.
pub fn resize(&mut self, area: Rect) -> io::Result<()> {
pub fn resize(&mut self, size: Rect) -> io::Result<()> {
let next_area = match self.viewport {
Viewport::Fullscreen => size,
Viewport::Inline(height) => {
let offset_in_previous_viewport = self
.last_known_cursor_pos
.1
.saturating_sub(self.viewport_area.top());
compute_inline_size(&mut self.backend, height, size, offset_in_previous_viewport)?.0
}
Viewport::Fixed(area) => area,
};
self.set_viewport_area(next_area);
self.clear()?;
self.last_known_size = size;
Ok(())
}
fn set_viewport_area(&mut self, area: Rect) {
self.buffers[self.current].resize(area);
self.buffers[1 - self.current].reset();
self.buffers[1 - self.current].resize(area);
self.viewport.area = area;
self.backend.clear()
self.viewport_area = area;
}
/// Queries the backend for size and resizes if it doesn't match the previous size.
pub fn autoresize(&mut self) -> io::Result<()> {
if self.viewport.resize_behavior == ResizeBehavior::Auto {
// fixed viewports do not get autoresized
if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) {
let size = self.size()?;
if size != self.viewport.area {
if size != self.last_known_size {
self.resize(size)?;
}
};
@@ -248,7 +266,7 @@ where
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
/// and prepares for the next draw call.
pub fn draw<F>(&mut self, f: F) -> io::Result<()>
pub fn draw<F>(&mut self, f: F) -> io::Result<CompletedFrame>
where
F: FnOnce(&mut Frame<B>),
{
@@ -280,7 +298,11 @@ where
// Flush
self.backend.flush()?;
Ok(())
Ok(CompletedFrame {
buffer: &self.buffers[1 - self.current],
area: self.last_known_size,
})
}
pub fn hide_cursor(&mut self) -> io::Result<()> {
@@ -300,15 +322,160 @@ where
}
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.backend.set_cursor(x, y)
self.backend.set_cursor(x, y)?;
self.last_known_cursor_pos = (x, y);
Ok(())
}
/// Clear the terminal and force a full redraw on the next draw call.
pub fn clear(&mut self) -> io::Result<()> {
self.backend.clear()
match self.viewport {
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
Viewport::Inline(_) => {
self.backend
.set_cursor(self.viewport_area.left(), self.viewport_area.top())?;
self.backend.clear_region(ClearType::AfterCursor)?;
}
Viewport::Fixed(area) => {
for row in area.top()..area.bottom() {
self.backend.set_cursor(0, row)?;
self.backend.clear_region(ClearType::AfterCursor)?;
}
}
}
// Reset the back buffer to make sure the next update will redraw everything.
self.buffers[1 - self.current].reset();
Ok(())
}
/// Queries the real size of the backend.
pub fn size(&self) -> io::Result<Rect> {
self.backend.size()
}
/// Insert some content before the current inline viewport. This has no effect when the
/// viewport is fullscreen.
///
/// This function scrolls down the current viewport by the given height. The newly freed space is
/// then made available to the `draw_fn` closure through a writable `Buffer`.
///
/// Before:
/// ```ignore
/// +-------------------+
/// | |
/// | viewport |
/// | |
/// +-------------------+
/// ```
///
/// After:
/// ```ignore
/// +-------------------+
/// | buffer |
/// +-------------------+
/// +-------------------+
/// | |
/// | viewport |
/// | |
/// +-------------------+
/// ```
///
/// # Examples
///
/// ## Insert a single line before the current viewport
///
/// ```rust
/// # use ratatui::widgets::{Paragraph, Widget};
/// # use ratatui::text::{Spans, Span};
/// # use ratatui::style::{Color, Style};
/// # use ratatui::{Terminal};
/// # use ratatui::backend::TestBackend;
/// # let backend = TestBackend::new(10, 10);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// terminal.insert_before(1, |buf| {
/// Paragraph::new(Spans::from(vec![
/// Span::raw("This line will be added "),
/// Span::styled("before", Style::default().fg(Color::Blue)),
/// Span::raw(" the current viewport")
/// ])).render(buf.area, buf);
/// });
/// ```
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> io::Result<()>
where
F: FnOnce(&mut Buffer),
{
if !matches!(self.viewport, Viewport::Inline(_)) {
return Ok(());
}
self.clear()?;
let height = height.min(self.last_known_size.height);
self.backend.append_lines(height)?;
let missing_lines =
height.saturating_sub(self.last_known_size.bottom() - self.viewport_area.top());
let area = Rect {
x: self.viewport_area.left(),
y: self.viewport_area.top().saturating_sub(missing_lines),
width: self.viewport_area.width,
height,
};
let mut buffer = Buffer::empty(area);
draw_fn(&mut buffer);
let iter = buffer.content.iter().enumerate().map(|(i, c)| {
let (x, y) = buffer.pos_of(i);
(x, y, c)
});
self.backend.draw(iter)?;
self.backend.flush()?;
let remaining_lines = self.last_known_size.height - area.bottom();
let missing_lines = self.viewport_area.height.saturating_sub(remaining_lines);
self.backend.append_lines(self.viewport_area.height)?;
self.set_viewport_area(Rect {
x: area.left(),
y: area.bottom().saturating_sub(missing_lines),
width: area.width,
height: self.viewport_area.height,
});
Ok(())
}
}
fn compute_inline_size<B: Backend>(
backend: &mut B,
height: u16,
size: Rect,
offset_in_previous_viewport: u16,
) -> io::Result<(Rect, (u16, u16))> {
let pos = backend.get_cursor()?;
let mut row = pos.1;
let max_height = size.height.min(height);
let lines_after_cursor = height
.saturating_sub(offset_in_previous_viewport)
.saturating_sub(1);
backend.append_lines(lines_after_cursor)?;
let available_lines = size.height.saturating_sub(row).saturating_sub(1);
let missing_lines = lines_after_cursor.saturating_sub(available_lines);
if missing_lines > 0 {
row = row.saturating_sub(missing_lines);
}
row = row.saturating_sub(offset_in_previous_viewport);
Ok((
Rect {
x: 0,
y: row,
width: size.width,
height: max_height,
},
pos,
))
}

View File

@@ -1,7 +1,7 @@
//! Primitives for styled text.
//!
//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish,
//! those strings may be associated to a set of styles. `tui` has three ways to represent them:
//! those strings may be associated to a set of styles. `ratatui` has three ways to represent them:
//! - A single line string where all graphemes have the same style is represented by a [`Span`].
//! - A single line string where each grapheme may have its own style is represented by [`Spans`].
//! - A multiple line string where each grapheme may have its own style is represented by a
@@ -11,7 +11,7 @@
//! is a [`Spans`].
//!
//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is
//! supported for their properties. Moreover, `tui` provides convenient `From` implementations so
//! supported for their properties. Moreover, `ratatui` provides convenient `From` implementations so
//! that you can start by using simple `String` or `&str` and then promote them to the previous
//! primitives when you need additional styling capabilities.
//!
@@ -19,9 +19,9 @@
//! its `title` property (which is a [`Spans`] under the hood):
//!
//! ```rust
//! # use tui::widgets::Block;
//! # use tui::text::{Span, Spans};
//! # use tui::style::{Color, Style};
//! # use ratatui::widgets::Block;
//! # use ratatui::text::{Span, Spans};
//! # use ratatui::style::{Color, Style};
//! // A simple string with no styling.
//! // Converted to Spans(vec![
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
@@ -47,19 +47,19 @@
//! ]);
//! ```
use crate::style::Style;
use std::{borrow::Cow, cmp::max};
use std::borrow::Cow;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
/// A grapheme associated to a style.
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StyledGrapheme<'a> {
pub symbol: &'a str,
pub style: Style,
}
/// A string where all graphemes have the same style.
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Span<'a> {
pub content: Cow<'a, str>,
pub style: Style,
@@ -71,7 +71,7 @@ impl<'a> Span<'a> {
/// ## Examples
///
/// ```rust
/// # use tui::text::Span;
/// # use ratatui::text::Span;
/// Span::raw("My text");
/// Span::raw(String::from("My text"));
/// ```
@@ -90,8 +90,8 @@ impl<'a> Span<'a> {
/// # Examples
///
/// ```rust
/// # use tui::text::Span;
/// # use tui::style::{Color, Modifier, Style};
/// # use ratatui::text::Span;
/// # use ratatui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// Span::styled("My text", style);
/// Span::styled(String::from("My text"), style);
@@ -119,8 +119,8 @@ impl<'a> Span<'a> {
/// ## Examples
///
/// ```rust
/// # use tui::text::{Span, StyledGrapheme};
/// # use tui::style::{Color, Modifier, Style};
/// # use ratatui::text::{Span, StyledGrapheme};
/// # use ratatui::style::{Color, Modifier, Style};
/// # use std::iter::Iterator;
/// let style = Style::default().fg(Color::Yellow);
/// let span = Span::styled("Text", style);
@@ -179,6 +179,43 @@ impl<'a> Span<'a> {
})
.filter(|s| s.symbol != "\n")
}
/// Patches the style an existing Span, adding modifiers from the given style.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::Span;
/// # use ratatui::style::{Color, Style, Modifier};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut raw_span = Span::raw("My text");
/// let mut styled_span = Span::styled("My text", style);
///
/// assert_ne!(raw_span, styled_span);
///
/// raw_span.patch_style(style);
/// assert_eq!(raw_span, styled_span);
/// ```
pub fn patch_style(&mut self, style: Style) {
self.style = self.style.patch(style);
}
/// Resets the style of the Span.
/// Equivalent to calling `patch_style(Style::reset())`.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::Span;
/// # use ratatui::style::{Color, Style, Modifier};
/// let mut span = Span::styled("My text", Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC));
///
/// span.reset_style();
/// assert_eq!(Style::reset(), span.style);
/// ```
pub fn reset_style(&mut self) {
self.patch_style(Style::reset());
}
}
impl<'a> From<String> for Span<'a> {
@@ -194,23 +231,17 @@ 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, Eq)]
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.
///
/// ## Examples
///
/// ```rust
/// # use tui::text::{Span, Spans};
/// # use tui::style::{Color, Style};
/// # use ratatui::text::{Span, Spans};
/// # use ratatui::style::{Color, Style};
/// let spans = Spans::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::raw(" text"),
@@ -218,7 +249,58 @@ impl<'a> Spans<'a> {
/// assert_eq!(7, spans.width());
/// ```
pub fn width(&self) -> usize {
self.0.iter().fold(0, |acc, s| acc + s.width())
self.0.iter().map(Span::width).sum()
}
/// Patches the style of each Span in an existing Spans, adding modifiers from the given style.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, Spans};
/// # use ratatui::style::{Color, Style, Modifier};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut raw_spans = Spans::from(vec![
/// Span::raw("My"),
/// Span::raw(" text"),
/// ]);
/// let mut styled_spans = Spans::from(vec![
/// Span::styled("My", style),
/// Span::styled(" text", style),
/// ]);
///
/// assert_ne!(raw_spans, styled_spans);
///
/// raw_spans.patch_style(style);
/// assert_eq!(raw_spans, styled_spans);
/// ```
pub fn patch_style(&mut self, style: Style) {
for span in &mut self.0 {
span.patch_style(style);
}
}
/// Resets the style of each Span in the Spans.
/// Equivalent to calling `patch_style(Style::reset())`.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, Spans};
/// # use ratatui::style::{Color, Style, Modifier};
/// let mut spans = Spans::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
/// ]);
///
/// spans.reset_style();
/// assert_eq!(Style::reset(), spans.0[0].style);
/// assert_eq!(Style::reset(), spans.0[1].style);
/// ```
pub fn reset_style(&mut self) {
for span in &mut self.0 {
span.reset_style();
}
}
}
@@ -257,29 +339,92 @@ impl<'a> From<Spans<'a>> for String {
/// A string split over multiple lines where each line is composed of several clusters, each with
/// their own style.
#[derive(Debug, Clone, PartialEq)]
///
/// A [`Text`], like a [`Span`], can be constructed using one of the many `From` implementations
/// or via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements
/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
///
/// ```rust
/// # use ratatui::text::Text;
/// # use ratatui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
///
/// // An initial two lines of `Text` built from a `&str`
/// let mut text = Text::from("The first line\nThe second line");
/// assert_eq!(2, text.height());
///
/// // Adding two more unstyled lines
/// text.extend(Text::raw("These are two\nmore lines!"));
/// assert_eq!(4, text.height());
///
/// // Adding a final two styled lines
/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
/// assert_eq!(6, text.height());
/// ```
#[derive(Debug, Clone, PartialEq, Default, Eq)]
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.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::Text;
/// Text::raw("The first line\nThe second line");
/// Text::raw(String::from("The first line\nThe second line"));
/// ```
pub fn raw<T>(content: T) -> Text<'a>
where
T: Into<Cow<'a, str>>,
{
let lines: Vec<_> = match content.into() {
Cow::Borrowed("") => vec![Spans::from("")],
Cow::Borrowed(s) => s.lines().map(Spans::from).collect(),
Cow::Owned(s) if s.is_empty() => vec![Spans::from("")],
Cow::Owned(s) => s.lines().map(|l| Spans::from(l.to_owned())).collect(),
};
Text { lines }
}
/// Create some text (potentially multiple lines) with a style.
///
/// # Examples
///
/// ```rust
/// # use ratatui::text::Text;
/// # use ratatui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// Text::styled("The first line\nThe second line", style);
/// Text::styled(String::from("The first line\nThe second line"), style);
/// ```
pub fn styled<T>(content: T, style: Style) -> Text<'a>
where
T: Into<Cow<'a, str>>,
{
let mut text = Text::raw(content);
text.patch_style(style);
text
}
/// Returns the max width of all the lines.
///
/// ## Examples
///
/// ```rust
/// use tui::text::Text;
/// use ratatui::text::Text;
/// let text = Text::from("The first line\nThe second line");
/// assert_eq!(15, text.width());
/// ```
pub fn width(&self) -> usize {
self.lines.iter().fold(0, |acc, l| max(acc, l.width()))
self.lines
.iter()
.map(Spans::width)
.max()
.unwrap_or_default()
}
/// Returns the height.
@@ -287,7 +432,7 @@ impl<'a> Text<'a> {
/// ## Examples
///
/// ```rust
/// use tui::text::Text;
/// use ratatui::text::Text;
/// let text = Text::from("The first line\nThe second line");
/// assert_eq!(2, text.height());
/// ```
@@ -295,20 +440,67 @@ impl<'a> Text<'a> {
self.lines.len()
}
/// Patches the style of each line in an existing Text, adding modifiers from the given style.
///
/// # Examples
///
/// ```rust
/// # use ratatui::text::Text;
/// # use ratatui::style::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut raw_text = Text::raw("The first line\nThe second line");
/// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
/// assert_ne!(raw_text, styled_text);
///
/// raw_text.patch_style(style);
/// assert_eq!(raw_text, styled_text);
/// ```
pub fn patch_style(&mut self, style: Style) {
for line in &mut self.lines {
for span in &mut line.0 {
span.style = span.style.patch(style);
}
line.patch_style(style);
}
}
/// Resets the style of the Text.
/// Equivalent to calling `patch_style(Style::reset())`.
///
/// ## Examples
///
/// ```rust
/// # use ratatui::text::{Span, Spans, Text};
/// # use ratatui::style::{Color, Style, Modifier};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut text = Text::styled("The first line\nThe second line", style);
///
/// text.reset_style();
/// for line in &text.lines {
/// for span in &line.0 {
/// assert_eq!(Style::reset(), span.style);
/// }
/// }
/// ```
pub fn reset_style(&mut self) {
for line in &mut self.lines {
line.reset_style();
}
}
}
impl<'a> From<String> for Text<'a> {
fn from(s: String) -> Text<'a> {
Text::raw(s)
}
}
impl<'a> From<&'a str> for Text<'a> {
fn from(s: &'a str) -> Text<'a> {
Text {
lines: s.lines().map(Spans::from).collect(),
}
Text::raw(s)
}
}
impl<'a> From<Cow<'a, str>> for Text<'a> {
fn from(s: Cow<'a, str>) -> Text<'a> {
Text::raw(s)
}
}
@@ -331,3 +523,18 @@ impl<'a> From<Vec<Spans<'a>>> for Text<'a> {
Text { lines }
}
}
impl<'a> IntoIterator for Text<'a> {
type Item = Spans<'a>;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.lines.into_iter()
}
}
impl<'a> Extend<Spans<'a>> for Text<'a> {
fn extend<T: IntoIterator<Item = Spans<'a>>>(&mut self, iter: T) {
self.lines.extend(iter);
}
}

View File

@@ -5,7 +5,7 @@ use crate::{
symbols,
widgets::{Block, Widget},
};
use std::cmp::{max, min};
use std::cmp::min;
use unicode_width::UnicodeWidthStr;
/// Display multiple bars in a single widgets
@@ -13,8 +13,8 @@ use unicode_width::UnicodeWidthStr;
/// # Examples
///
/// ```
/// # use tui::widgets::{Block, Borders, BarChart};
/// # use tui::style::{Style, Color, Modifier};
/// # use ratatui::widgets::{Block, Borders, BarChart};
/// # use ratatui::style::{Style, Color, Modifier};
/// BarChart::default()
/// .block(Block::default().title("BarChart").borders(Borders::ALL))
/// .bar_width(3)
@@ -145,7 +145,7 @@ impl<'a> Widget for BarChart<'a> {
let max = self
.max
.unwrap_or_else(|| self.data.iter().fold(0, |acc, &(_, v)| max(v, acc)));
.unwrap_or_else(|| self.data.iter().map(|t| t.1).max().unwrap_or_default());
let max_index = min(
(chart_area.width / (self.bar_width + self.bar_gap)) as usize,
self.data.len(),
@@ -157,7 +157,7 @@ impl<'a> Widget for BarChart<'a> {
.map(|&(l, v)| {
(
l,
v * u64::from(chart_area.height) * 8 / std::cmp::max(max, 1),
v * u64::from(chart_area.height - 1) * 8 / std::cmp::max(max, 1),
)
})
.collect::<Vec<(&str, u64)>>();

View File

@@ -1,13 +1,13 @@
use crate::{
buffer::Buffer,
layout::Rect,
layout::{Alignment, Rect},
style::Style,
symbols::line,
text::{Span, Spans},
widgets::{Borders, Widget},
};
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BorderType {
Plain,
Rounded,
@@ -26,14 +26,69 @@ impl BorderType {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Padding {
pub left: u16,
pub right: u16,
pub top: u16,
pub bottom: u16,
}
impl Padding {
pub fn new(left: u16, right: u16, top: u16, bottom: u16) -> Self {
Padding {
left,
right,
top,
bottom,
}
}
pub fn zero() -> Self {
Padding {
left: 0,
right: 0,
top: 0,
bottom: 0,
}
}
pub fn horizontal(value: u16) -> Self {
Padding {
left: value,
right: value,
top: 0,
bottom: 0,
}
}
pub fn vertical(value: u16) -> Self {
Padding {
left: 0,
right: 0,
top: value,
bottom: value,
}
}
pub fn uniform(value: u16) -> Self {
Padding {
left: value,
right: value,
top: value,
bottom: value,
}
}
}
/// Base widget to be used with all upper level ones. It may be used to display a box border around
/// the widget and/or add a title.
///
/// # Examples
///
/// ```
/// # use tui::widgets::{Block, BorderType, Borders};
/// # use tui::style::{Style, Color};
/// # use ratatui::widgets::{Block, BorderType, Borders};
/// # use ratatui::style::{Style, Color};
/// Block::default()
/// .title("Block")
/// .borders(Borders::LEFT | Borders::RIGHT)
@@ -41,10 +96,15 @@ impl BorderType {
/// .border_type(BorderType::Rounded)
/// .style(Style::default().bg(Color::Black));
/// ```
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
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,
/// Whether or not title goes on top or bottom row of the block
title_on_bottom: bool,
/// Visible borders
borders: Borders,
/// Border style
@@ -54,16 +114,21 @@ pub struct Block<'a> {
border_type: BorderType,
/// Widget style
style: Style,
/// Block padding
padding: Padding,
}
impl<'a> Default for Block<'a> {
fn default() -> Block<'a> {
Block {
title: None,
title_alignment: Alignment::Left,
title_on_bottom: false,
borders: Borders::NONE,
border_style: Default::default(),
border_type: BorderType::Plain,
style: Default::default(),
padding: Padding::zero(),
}
}
}
@@ -89,6 +154,16 @@ impl<'a> Block<'a> {
self
}
pub fn title_alignment(mut self, alignment: Alignment) -> Block<'a> {
self.title_alignment = alignment;
self
}
pub fn title_on_bottom(mut self) -> Block<'a> {
self.title_on_bottom = true;
self
}
pub fn border_style(mut self, style: Style) -> Block<'a> {
self.border_style = style;
self
@@ -110,38 +185,78 @@ impl<'a> Block<'a> {
}
/// Compute the inner area of a block based on its border visibility rules.
///
/// # Examples
///
/// ```
/// // Draw a block nested within another block
/// use ratatui::{backend::TestBackend, buffer::Buffer, terminal::Terminal, widgets::{Block, Borders}};
/// let backend = TestBackend::new(15, 5);
/// let mut terminal = Terminal::new(backend).unwrap();
/// let outer_block = Block::default()
/// .title("Outer Block")
/// .borders(Borders::ALL);
/// let inner_block = Block::default()
/// .title("Inner Block")
/// .borders(Borders::ALL);
/// terminal.draw(|f| {
/// let inner_area = outer_block.inner(f.size());
/// f.render_widget(outer_block, f.size());
/// f.render_widget(inner_block, inner_area);
/// });
/// let expected = Buffer::with_lines(vec![
/// "┌Outer Block──┐",
/// "│┌Inner Block┐│",
/// "││ ││",
/// "│└───────────┘│",
/// "└─────────────┘",
/// ]);
/// terminal.backend().assert_buffer(&expected);
/// ```
pub fn inner(&self, area: Rect) -> Rect {
if area.width < 2 || area.height < 2 {
return Rect::default();
}
let mut inner = area;
if self.borders.intersects(Borders::LEFT) {
inner.x += 1;
inner.width -= 1;
inner.x = inner.x.saturating_add(1).min(inner.right());
inner.width = inner.width.saturating_sub(1);
}
if self.borders.intersects(Borders::TOP) || self.title.is_some() {
inner.y += 1;
inner.height -= 1;
inner.y = inner.y.saturating_add(1).min(inner.bottom());
inner.height = inner.height.saturating_sub(1);
}
if self.borders.intersects(Borders::RIGHT) {
inner.width -= 1;
inner.width = inner.width.saturating_sub(1);
}
if self.borders.intersects(Borders::BOTTOM) {
inner.height -= 1;
inner.height = inner.height.saturating_sub(1);
}
inner.x = inner.x.saturating_add(self.padding.left);
inner.y = inner.y.saturating_add(self.padding.top);
inner.width = inner
.width
.saturating_sub(self.padding.left + self.padding.right);
inner.height = inner
.height
.saturating_sub(self.padding.top + self.padding.bottom);
inner
}
pub fn padding(mut self, padding: Padding) -> Block<'a> {
self.padding = padding;
self
}
}
impl<'a> Widget for Block<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
if area.width < 2 || area.height < 2 {
if area.area() == 0 {
return;
}
buf.set_style(area, self.style);
let symbols = BorderType::line_symbols(self.border_type);
// Sides
if self.borders.intersects(Borders::LEFT) {
for y in area.top()..area.bottom() {
@@ -175,9 +290,9 @@ impl<'a> Widget for Block<'a> {
}
// Corners
if self.borders.contains(Borders::LEFT | Borders::TOP) {
buf.get_mut(area.left(), area.top())
.set_symbol(symbols.top_left)
if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
buf.get_mut(area.right() - 1, area.bottom() - 1)
.set_symbol(symbols.bottom_right)
.set_style(self.border_style);
}
if self.borders.contains(Borders::RIGHT | Borders::TOP) {
@@ -190,25 +305,383 @@ impl<'a> Widget for Block<'a> {
.set_symbol(symbols.bottom_left)
.set_style(self.border_style);
}
if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
buf.get_mut(area.right() - 1, area.bottom() - 1)
.set_symbol(symbols.bottom_right)
if self.borders.contains(Borders::LEFT | Borders::TOP) {
buf.get_mut(area.left(), area.top())
.set_symbol(symbols.top_left)
.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 - lx - 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 = if self.title_on_bottom {
area.bottom() - 1
} else {
area.top()
};
buf.set_spans(title_x, title_y, &title, title_area_width);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::Rect;
#[test]
fn inner_takes_into_account_the_borders() {
// No borders
assert_eq!(
Block::default().inner(Rect::default()),
Rect {
x: 0,
y: 0,
width: 0,
height: 0
},
"no borders, width=0, height=0"
);
assert_eq!(
Block::default().inner(Rect {
x: 0,
y: 0,
width: 1,
height: 1
}),
Rect {
x: 0,
y: 0,
width: 1,
height: 1
},
"no borders, width=1, height=1"
);
// Left border
assert_eq!(
Block::default().borders(Borders::LEFT).inner(Rect {
x: 0,
y: 0,
width: 0,
height: 1
}),
Rect {
x: 0,
y: 0,
width: 0,
height: 1
},
"left, width=0"
);
assert_eq!(
Block::default().borders(Borders::LEFT).inner(Rect {
x: 0,
y: 0,
width: 1,
height: 1
}),
Rect {
x: 1,
y: 0,
width: 0,
height: 1
},
"left, width=1"
);
assert_eq!(
Block::default().borders(Borders::LEFT).inner(Rect {
x: 0,
y: 0,
width: 2,
height: 1
}),
Rect {
x: 1,
y: 0,
width: 1,
height: 1
},
"left, width=2"
);
// Top border
assert_eq!(
Block::default().borders(Borders::TOP).inner(Rect {
x: 0,
y: 0,
width: 1,
height: 0
}),
Rect {
x: 0,
y: 0,
width: 1,
height: 0
},
"top, height=0"
);
assert_eq!(
Block::default().borders(Borders::TOP).inner(Rect {
x: 0,
y: 0,
width: 1,
height: 1
}),
Rect {
x: 0,
y: 1,
width: 1,
height: 0
},
"top, height=1"
);
assert_eq!(
Block::default().borders(Borders::TOP).inner(Rect {
x: 0,
y: 0,
width: 1,
height: 2
}),
Rect {
x: 0,
y: 1,
width: 1,
height: 1
},
"top, height=2"
);
// Right border
assert_eq!(
Block::default().borders(Borders::RIGHT).inner(Rect {
x: 0,
y: 0,
width: 0,
height: 1
}),
Rect {
x: 0,
y: 0,
width: 0,
height: 1
},
"right, width=0"
);
assert_eq!(
Block::default().borders(Borders::RIGHT).inner(Rect {
x: 0,
y: 0,
width: 1,
height: 1
}),
Rect {
x: 0,
y: 0,
width: 0,
height: 1
},
"right, width=1"
);
assert_eq!(
Block::default().borders(Borders::RIGHT).inner(Rect {
x: 0,
y: 0,
width: 2,
height: 1
}),
Rect {
x: 0,
y: 0,
width: 1,
height: 1
},
"right, width=2"
);
// Bottom border
assert_eq!(
Block::default().borders(Borders::BOTTOM).inner(Rect {
x: 0,
y: 0,
width: 1,
height: 0
}),
Rect {
x: 0,
y: 0,
width: 1,
height: 0
},
"bottom, height=0"
);
assert_eq!(
Block::default().borders(Borders::BOTTOM).inner(Rect {
x: 0,
y: 0,
width: 1,
height: 1
}),
Rect {
x: 0,
y: 0,
width: 1,
height: 0
},
"bottom, height=1"
);
assert_eq!(
Block::default().borders(Borders::BOTTOM).inner(Rect {
x: 0,
y: 0,
width: 1,
height: 2
}),
Rect {
x: 0,
y: 0,
width: 1,
height: 1
},
"bottom, height=2"
);
// All borders
assert_eq!(
Block::default()
.borders(Borders::ALL)
.inner(Rect::default()),
Rect {
x: 0,
y: 0,
width: 0,
height: 0
},
"all borders, width=0, height=0"
);
assert_eq!(
Block::default().borders(Borders::ALL).inner(Rect {
x: 0,
y: 0,
width: 1,
height: 1
}),
Rect {
x: 1,
y: 1,
width: 0,
height: 0,
},
"all borders, width=1, height=1"
);
assert_eq!(
Block::default().borders(Borders::ALL).inner(Rect {
x: 0,
y: 0,
width: 2,
height: 2,
}),
Rect {
x: 1,
y: 1,
width: 0,
height: 0,
},
"all borders, width=2, height=2"
);
assert_eq!(
Block::default().borders(Borders::ALL).inner(Rect {
x: 0,
y: 0,
width: 3,
height: 3,
}),
Rect {
x: 1,
y: 1,
width: 1,
height: 1,
},
"all borders, width=3, height=3"
);
}
#[test]
fn inner_takes_into_account_the_title() {
assert_eq!(
Block::default().title("Test").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::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,
},
);
}
}

245
src/widgets/calendar.rs Normal file
View File

@@ -0,0 +1,245 @@
//! A simple calendar widget. `(feature: widget-calendar)`
//!
//!
//!
//! The [`Monthly`] widget will display a calendar for the monh provided in `display_date`. Days are
//! styled using the default style unless:
//! * `show_surrounding` is set, then days not in the `display_date` month will use that style.
//! * a style is returned by the [`DateStyler`] for the day
//!
//! [`Monthly`] has several controls for what should be displayed
use std::collections::HashMap;
use crate::{
buffer::Buffer,
layout::Rect,
style::Style,
text::{Span, Spans},
widgets::{Block, Widget},
};
use time::{Date, Duration, OffsetDateTime};
/// Display a month calendar for the month containing `display_date`
pub struct Monthly<'a, S: DateStyler> {
display_date: Date,
events: S,
show_surrounding: Option<Style>,
show_weekday: Option<Style>,
show_month: Option<Style>,
default_style: Style,
block: Option<Block<'a>>,
}
impl<'a, S: DateStyler> Monthly<'a, S> {
/// Construct a calendar for the `display_date` and highlight the `events`
pub fn new(display_date: Date, events: S) -> Self {
Self {
display_date,
events,
show_surrounding: None,
show_weekday: None,
show_month: None,
default_style: Style::default(),
block: None,
}
}
/// Fill the calendar slots for days not in the current month also, this causes each line to be
/// completely filled. If there is an event style for a date, this style will be patched with
/// the event's style
pub fn show_surrounding(mut self, style: Style) -> Self {
self.show_surrounding = Some(style);
self
}
/// Display a header containing weekday abbreviations
pub fn show_weekdays_header(mut self, style: Style) -> Self {
self.show_weekday = Some(style);
self
}
/// Display a header containing the month and year
pub fn show_month_header(mut self, style: Style) -> Self {
self.show_month = Some(style);
self
}
/// How to render otherwise unstyled dates
pub fn default_style(mut self, s: Style) -> Self {
self.default_style = s;
self
}
/// Render the calendar within a [Block](ratatui::widgets::Block)
pub fn block(mut self, b: Block<'a>) -> Self {
self.block = Some(b);
self
}
/// Return a style with only the background from the default style
fn default_bg(&self) -> Style {
match self.default_style.bg {
None => Style::default(),
Some(c) => Style::default().bg(c),
}
}
/// All logic to style a date goes here.
fn format_date(&self, date: Date) -> Span {
if date.month() != self.display_date.month() {
match self.show_surrounding {
None => Span::styled(" ", self.default_bg()),
Some(s) => {
let style = self
.default_style
.patch(s)
.patch(self.events.get_style(date));
Span::styled(format!("{:2?}", date.day()), style)
}
}
} else {
Span::styled(
format!("{:2?}", date.day()),
self.default_style.patch(self.events.get_style(date)),
)
}
}
}
impl<'a, S: DateStyler> Widget for Monthly<'a, S> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
// Block is used for borders and such
// Draw that first, and use the blank area inside the block for our own purposes
let mut area = match self.block.take() {
None => area,
Some(b) => {
let inner = b.inner(area);
b.render(area, buf);
inner
}
};
// Draw the month name and year
if let Some(style) = self.show_month {
let line = Span::styled(
format!("{} {}", self.display_date.month(), self.display_date.year()),
style,
);
// cal is 21 cells wide, so hard code the 11
let x_off = 11_u16.saturating_sub(line.width() as u16 / 2);
buf.set_spans(area.x + x_off, area.y, &line.into(), area.width);
area.y += 1
}
// Draw days of week
if let Some(style) = self.show_weekday {
let days = String::from(" Su Mo Tu We Th Fr Sa");
buf.set_string(area.x, area.y, days, style);
area.y += 1;
}
// Set the start of the calendar to the Sunday before the 1st (or the sunday of the first)
let first_of_month = self.display_date.replace_day(1).unwrap();
let offset = Duration::days(first_of_month.weekday().number_days_from_sunday().into());
let mut curr_day = first_of_month - offset;
// go through all the weeks containing a day in the target month.
while curr_day.month() as u8 != self.display_date.month().next() as u8 {
let mut line = Spans(Vec::with_capacity(14));
for i in 0..7 {
// Draw the gutter. Do it here so we can avoid worrying about
// styling the ' ' in the format_date method
if i == 0 {
line.0.push(Span::styled(" ", Style::default()));
} else {
line.0.push(Span::styled(" ", self.default_bg()));
}
line.0.push(self.format_date(curr_day));
curr_day += Duration::DAY;
}
buf.set_spans(area.x, area.y, &line, area.width);
area.y += 1;
}
}
}
/// Provides a method for styling a given date. [Month] is generic on this trait, so any type
/// that implements this trait can be used.
pub trait DateStyler {
/// Given a date, return a style for that date
fn get_style(&self, date: Date) -> Style;
}
/// A simple DateStyler based on a [HashMap]
pub struct CalendarEventStore(pub HashMap<Date, Style>);
impl CalendarEventStore {
/// Construct a store that has the current date styled.
pub fn today(style: Style) -> Self {
let mut res = Self::default();
res.add(OffsetDateTime::now_local().unwrap().date(), style);
res
}
/// Add a date and style to the store
pub fn add(&mut self, date: Date, style: Style) {
// to simplify style nonsense, last write wins
let _ = self.0.insert(date, style);
}
/// Helper for trait impls
fn lookup_style(&self, date: Date) -> Style {
self.0.get(&date).copied().unwrap_or_default()
}
}
impl DateStyler for CalendarEventStore {
fn get_style(&self, date: Date) -> Style {
self.lookup_style(date)
}
}
impl DateStyler for &CalendarEventStore {
fn get_style(&self, date: Date) -> Style {
self.lookup_style(date)
}
}
impl Default for CalendarEventStore {
fn default() -> Self {
Self(HashMap::with_capacity(4))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Color;
use time::Month;
#[test]
fn event_store() {
let a = (
Date::from_calendar_date(2023, Month::January, 1).unwrap(),
Style::default(),
);
let b = (
Date::from_calendar_date(2023, Month::January, 2).unwrap(),
Style::default().bg(Color::Red).fg(Color::Blue),
);
let mut s = CalendarEventStore::default();
s.add(b.0, b.1);
assert_eq!(
s.get_style(a.0),
a.1,
"Date not added to the styler should look up as Style::default()"
);
assert_eq!(
s.get_style(b.0),
b.1,
"Date added to styler should return the provided style"
);
}
}

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)]
@@ -111,26 +111,28 @@ impl Grid for BrailleGrid {
}
#[derive(Debug, Clone)]
struct DotGrid {
struct CharGrid {
width: u16,
height: u16,
cells: Vec<char>,
colors: Vec<Color>,
cell_char: char,
}
impl DotGrid {
fn new(width: u16, height: u16) -> DotGrid {
impl CharGrid {
fn new(width: u16, height: u16, cell_char: char) -> CharGrid {
let length = usize::from(width * height);
DotGrid {
CharGrid {
width,
height,
cells: vec![' '; length],
colors: vec![Color::Reset; length],
cell_char,
}
}
}
impl Grid for DotGrid {
impl Grid for CharGrid {
fn width(&self) -> u16 {
self.width
}
@@ -162,7 +164,7 @@ impl Grid for DotGrid {
fn paint(&mut self, x: usize, y: usize, color: Color) {
let index = y * self.width as usize + x;
if let Some(c) = self.cells.get_mut(index) {
*c = '•';
*c = self.cell_char;
}
if let Some(c) = self.colors.get_mut(index) {
*c = color;
@@ -181,7 +183,7 @@ impl<'a, 'b> Painter<'a, 'b> {
///
/// # Examples:
/// ```
/// use tui::{symbols, widgets::canvas::{Painter, Context}};
/// use ratatui::{symbols, widgets::canvas::{Painter, Context}};
///
/// let mut ctx = Context::new(2, 2, [1.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
/// let mut painter = Painter::from(&mut ctx);
@@ -218,7 +220,7 @@ impl<'a, 'b> Painter<'a, 'b> {
///
/// # Examples:
/// ```
/// use tui::{style::Color, symbols, widgets::canvas::{Painter, Context}};
/// use ratatui::{style::Color, symbols, widgets::canvas::{Painter, Context}};
///
/// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
/// let mut painter = Painter::from(&mut ctx);
@@ -258,8 +260,13 @@ impl<'a> Context<'a> {
y_bounds: [f64; 2],
marker: symbols::Marker,
) -> Context<'a> {
let dot = symbols::DOT.chars().next().unwrap();
let block = symbols::block::FULL.chars().next().unwrap();
let bar = symbols::bar::HALF.chars().next().unwrap();
let grid: Box<dyn Grid> = match marker {
symbols::Marker::Dot => Box::new(DotGrid::new(width, height)),
symbols::Marker::Dot => Box::new(CharGrid::new(width, height, dot)),
symbols::Marker::Block => Box::new(CharGrid::new(width, height, block)),
symbols::Marker::Bar => Box::new(CharGrid::new(width, height, bar)),
symbols::Marker::Braille => Box::new(BrailleGrid::new(width, height)),
};
Context {
@@ -290,8 +297,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
@@ -307,10 +321,10 @@ impl<'a> Context<'a> {
/// # Examples
///
/// ```
/// # use tui::widgets::{Block, Borders};
/// # use tui::layout::Rect;
/// # use tui::widgets::canvas::{Canvas, Shape, Line, Rectangle, Map, MapResolution};
/// # use tui::style::Color;
/// # use ratatui::widgets::{Block, Borders};
/// # use ratatui::layout::Rect;
/// # use ratatui::widgets::canvas::{Canvas, Shape, Line, Rectangle, Map, MapResolution};
/// # use ratatui::style::Color;
/// Canvas::default()
/// .block(Block::default().title("Canvas").borders(Borders::ALL))
/// .x_bounds([-180.0, 180.0])
@@ -374,11 +388,18 @@ where
self
}
/// Define the viewport of the canvas.
/// If you were to "zoom" to a certain part of the world you may want to choose different
/// bounds.
pub fn x_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> {
self.x_bounds = bounds;
self
}
/// Define the viewport of the canvas.
///
/// If you were to "zoom" to a certain part of the world you may want to choose different
/// bounds.
pub fn y_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> {
self.y_bounds = bounds;
self
@@ -396,17 +417,19 @@ where
}
/// Change the type of points used to draw the shapes. By default the braille patterns are used
/// as they provide a more fine grained result but you might want to use the simple dot instead
/// if the targeted terminal does not support those symbols.
/// as they provide a more fine grained result but you might want to use the simple dot or
/// block instead if the targeted terminal does not support those symbols.
///
/// # Examples
///
/// ```
/// # use tui::widgets::canvas::Canvas;
/// # use tui::symbols;
/// # use ratatui::widgets::canvas::Canvas;
/// # use ratatui::symbols;
/// Canvas::default().marker(symbols::Marker::Braille).paint(|ctx| {});
///
/// Canvas::default().marker(symbols::Marker::Dot).paint(|ctx| {});
///
/// Canvas::default().marker(symbols::Marker::Block).paint(|ctx| {});
/// ```
pub fn marker(mut self, marker: symbols::Marker) -> Canvas<'a, F> {
self.marker = marker;
@@ -428,6 +451,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 {
@@ -447,7 +472,7 @@ where
painter(&mut ctx);
ctx.finish();
// Retreive painted points for each layer
// Retrieve painted points for each layer
for layer in ctx.layers {
for (i, (ch, color)) in layer
.string
@@ -459,14 +484,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];
@@ -485,13 +508,107 @@ 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);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{buffer::Cell, symbols::Marker};
use indoc::indoc;
// helper to test the canvas checks that drawing a vertical and horizontal line
// results in the expected output
fn test_marker(marker: Marker, expected: &str) {
let area = Rect::new(0, 0, 5, 5);
let mut cell = Cell::default();
cell.set_char('x');
let mut buf = Buffer::filled(area, &cell);
let horizontal_line = Line {
x1: 0.0,
y1: 0.0,
x2: 10.0,
y2: 0.0,
color: Color::Reset,
};
let vertical_line = Line {
x1: 0.0,
y1: 0.0,
x2: 0.0,
y2: 10.0,
color: Color::Reset,
};
Canvas::default()
.marker(marker)
.paint(|ctx| {
ctx.draw(&vertical_line);
ctx.draw(&horizontal_line);
})
.x_bounds([0.0, 10.0])
.y_bounds([0.0, 10.0])
.render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines(expected.lines().collect()));
}
#[test]
fn test_bar_marker() {
test_marker(
Marker::Bar,
indoc!(
"
▄xxxx
▄xxxx
▄xxxx
▄xxxx
▄▄▄▄▄"
),
);
}
#[test]
fn test_block_marker() {
test_marker(
Marker::Block,
indoc!(
"
█xxxx
█xxxx
█xxxx
█xxxx
█████"
),
);
}
#[test]
fn test_braille_marker() {
test_marker(
Marker::Braille,
indoc!(
"
⡇xxxx
⡇xxxx
⡇xxxx
⡇xxxx
⣇⣀⣀⣀⣀"
),
);
}
#[test]
fn test_dot_marker() {
test_marker(
Marker::Dot,
indoc!(
"
•xxxx
•xxxx
•xxxx
•xxxx
•••••"
),
);
}
}

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,30 +176,15 @@ 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
///
/// ```
/// # use tui::symbols;
/// # use tui::widgets::{Block, Borders, Chart, Axis, Dataset, GraphType};
/// # use tui::style::{Style, Color};
/// # use tui::text::Span;
/// # use ratatui::symbols;
/// # use ratatui::widgets::{Block, Borders, Chart, Axis, Dataset, GraphType};
/// # use ratatui::style::{Style, Color};
/// # use ratatui::text::Span;
/// let datasets = vec![
/// Dataset::default()
/// .name("data1")
@@ -265,8 +265,8 @@ impl<'a> Chart<'a> {
/// # Examples
///
/// ```
/// # use tui::widgets::Chart;
/// # use tui::layout::Constraint;
/// # use ratatui::widgets::Chart;
/// # use ratatui::layout::Constraint;
/// let constraints = (
/// Constraint::Ratio(1, 3),
/// Constraint::Ratio(1, 4)
@@ -296,21 +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()
.fold(0, |acc, l| max(l.content.width(), acc))
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);
@@ -336,7 +323,7 @@ impl<'a> Chart<'a> {
if let Some(ref title) = self.y_axis.title {
let w = title.width() as u16;
if w + 1 < layout.graph_area.width && layout.graph_area.height > 2 {
layout.title_y = Some((x + 1, area.top()));
layout.title_y = Some((x, area.top()));
}
}
@@ -365,11 +352,156 @@ 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> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
if area.area() == 0 {
return;
}
buf.set_style(area, self.style);
// Sample the style of the entire widget. This sample will be used to reset the style of
// the cells that are part of the components put on top of the grah area (i.e legend and
// axis names).
let original_style = buf.get(area.left(), area.top()).style();
let chart_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
@@ -385,43 +517,8 @@ impl<'a> Widget for Chart<'a> {
return;
}
if let Some((x, y)) = layout.title_x {
let title = self.x_axis.title.unwrap();
buf.set_spans(x, y, &title, graph_area.right().saturating_sub(x));
}
if let Some((x, y)) = layout.title_y {
let title = self.y_axis.title.unwrap();
buf.set_spans(x, y, &title, graph_area.right().saturating_sub(x));
}
if let Some(y) = layout.label_x {
let labels = self.x_axis.labels.unwrap();
let total_width = labels.iter().fold(0, |acc, l| l.content.width() + acc) 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() {
@@ -474,6 +571,7 @@ impl<'a> Widget for Chart<'a> {
}
if let Some(legend_area) = layout.legend_area {
buf.set_style(legend_area, original_style);
Block::default()
.borders(Borders::ALL)
.render(legend_area, buf);
@@ -486,6 +584,36 @@ impl<'a> Widget for Chart<'a> {
);
}
}
if let Some((x, y)) = layout.title_x {
let title = self.x_axis.title.unwrap();
let width = graph_area.right().saturating_sub(x);
buf.set_style(
Rect {
x,
y,
width,
height: 1,
},
original_style,
);
buf.set_spans(x, y, &title, width);
}
if let Some((x, y)) = layout.title_y {
let title = self.y_axis.title.unwrap();
let width = graph_area.right().saturating_sub(x);
buf.set_style(
Rect {
x,
y,
width,
height: 1,
},
original_style,
);
buf.set_spans(x, y, &title, width);
}
}
}

View File

@@ -1,16 +1,17 @@
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 `ratatui` assumes the
/// render area is empty. Use [`crate::Terminal::clear`] instead.
///
/// # Examples
///
/// ```
/// # use tui::widgets::{Clear, Block, Borders};
/// # use tui::layout::Rect;
/// # use tui::Frame;
/// # use tui::backend::Backend;
/// # use ratatui::widgets::{Clear, Block, Borders};
/// # use ratatui::layout::Rect;
/// # use ratatui::Frame;
/// # use ratatui::backend::Backend;
/// fn draw_on_clear<B: Backend>(f: &mut Frame<B>, area: Rect) {
/// let block = Block::default().title("Block").borders(Borders::ALL);
/// f.render_widget(Clear, area); // <- this will clear/reset the area first

View File

@@ -2,7 +2,8 @@ use crate::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
text::Span,
symbols,
text::{Span, Spans},
widgets::{Block, Widget},
};
@@ -11,8 +12,8 @@ use crate::{
/// # Examples:
///
/// ```
/// # use tui::widgets::{Widget, Gauge, Block, Borders};
/// # use tui::style::{Style, Color, Modifier};
/// # use ratatui::widgets::{Widget, Gauge, Block, Borders};
/// # use ratatui::style::{Style, Color, Modifier};
/// Gauge::default()
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
/// .gauge_style(Style::default().fg(Color::White).bg(Color::Black).add_modifier(Modifier::ITALIC))
@@ -23,6 +24,7 @@ pub struct Gauge<'a> {
block: Option<Block<'a>>,
ratio: f64,
label: Option<Span<'a>>,
use_unicode: bool,
style: Style,
gauge_style: Style,
}
@@ -33,6 +35,7 @@ impl<'a> Default for Gauge<'a> {
block: None,
ratio: 0.0,
label: None,
use_unicode: false,
style: Style::default(),
gauge_style: Style::default(),
}
@@ -57,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;
@@ -81,6 +84,11 @@ impl<'a> Gauge<'a> {
self.gauge_style = style;
self
}
pub fn use_unicode(mut self, unicode: bool) -> Gauge<'a> {
self.use_unicode = unicode;
self
}
}
impl<'a> Widget for Gauge<'a> {
@@ -99,32 +107,184 @@ 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).round() as u16;
let end = gauge_area.left() + width;
// 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
for x in gauge_area.left()..end {
buf.get_mut(x, y).set_symbol(" ");
}
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);
}
// Fix colors
// render the filled area (left to end)
for x in gauge_area.left()..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 {
1 => symbols::block::ONE_EIGHTH,
2 => symbols::block::ONE_QUARTER,
3 => symbols::block::THREE_EIGHTHS,
4 => symbols::block::HALF,
5 => symbols::block::FIVE_EIGHTHS,
6 => symbols::block::THREE_QUARTERS,
7 => symbols::block::SEVEN_EIGHTHS,
8 => symbols::block::FULL,
_ => " ",
}
}
/// A compact widget to display a task progress over a single line.
///
/// # Examples:
///
/// ```
/// # use ratatui::widgets::{Widget, LineGauge, Block, Borders};
/// # use ratatui::style::{Style, Color, Modifier};
/// # use ratatui::symbols;
/// LineGauge::default()
/// .block(Block::default().borders(Borders::ALL).title("Progress"))
/// .gauge_style(Style::default().fg(Color::White).bg(Color::Black).add_modifier(Modifier::BOLD))
/// .line_set(symbols::line::THICK)
/// .ratio(0.4);
/// ```
pub struct LineGauge<'a> {
block: Option<Block<'a>>,
ratio: f64,
label: Option<Spans<'a>>,
line_set: symbols::line::Set,
style: Style,
gauge_style: Style,
}
impl<'a> Default for LineGauge<'a> {
fn default() -> Self {
Self {
block: None,
ratio: 0.0,
label: None,
style: Style::default(),
line_set: symbols::line::NORMAL,
gauge_style: Style::default(),
}
}
}
impl<'a> LineGauge<'a> {
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
pub fn ratio(mut self, ratio: f64) -> Self {
assert!(
(0.0..=1.0).contains(&ratio),
"Ratio should be between 0 and 1 inclusively."
);
self.ratio = ratio;
self
}
pub fn line_set(mut self, set: symbols::line::Set) -> Self {
self.line_set = set;
self
}
pub fn label<T>(mut self, label: T) -> Self
where
T: Into<Spans<'a>>,
{
self.label = Some(label.into());
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn gauge_style(mut self, style: Style) -> Self {
self.gauge_style = style;
self
}
}
impl<'a> Widget for LineGauge<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
let gauge_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
}
None => area,
};
if gauge_area.height < 1 {
return;
}
let ratio = self.ratio;
let label = self
.label
.unwrap_or_else(move || Spans::from(format!("{:.0}%", ratio * 100.0)));
let (col, row) = buf.set_spans(
gauge_area.left(),
gauge_area.top(),
&label,
gauge_area.width,
);
let start = col + 1;
if start >= gauge_area.right() {
return;
}
let end = start
+ (f64::from(gauge_area.right().saturating_sub(start)) * self.ratio).floor() as u16;
for col in start..end {
buf.get_mut(col, row)
.set_symbol(self.line_set.horizontal)
.set_style(Style {
fg: self.gauge_style.fg,
bg: None,
add_modifier: self.gauge_style.add_modifier,
sub_modifier: self.gauge_style.sub_modifier,
});
}
for col in end..gauge_area.right() {
buf.get_mut(col, row)
.set_symbol(self.line_set.horizontal)
.set_style(Style {
fg: self.gauge_style.bg,
bg: None,
add_modifier: self.gauge_style.add_modifier,
sub_modifier: self.gauge_style.sub_modifier,
});
}
}
}

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
@@ -36,7 +26,7 @@ impl ListState {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListItem<'a> {
content: Text<'a>,
style: Style,
@@ -61,6 +51,10 @@ impl<'a> ListItem<'a> {
pub fn height(&self) -> usize {
self.content.height()
}
pub fn width(&self) -> usize {
self.content.width()
}
}
/// A widget to display several items among which one can be selected (optional)
@@ -68,8 +62,8 @@ impl<'a> ListItem<'a> {
/// # Examples
///
/// ```
/// # use tui::widgets::{Block, Borders, List, ListItem};
/// # use tui::style::{Style, Color, Modifier};
/// # use ratatui::widgets::{Block, Borders, List, ListItem};
/// # use ratatui::style::{Style, Color, Modifier};
/// let items = [ListItem::new("Item 1"), ListItem::new("Item 2"), ListItem::new("Item 3")];
/// List::new(items)
/// .block(Block::default().title("List").borders(Borders::ALL))
@@ -88,6 +82,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 +98,7 @@ impl<'a> List<'a> {
start_corner: Corner::TopLeft,
highlight_style: Style::default(),
highlight_symbol: None,
repeat_highlight_symbol: false,
}
}
@@ -125,10 +122,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 +194,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,20 +230,28 @@ 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 highlight 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() {
buf.set_spans(elem_x, y + j as u16, line, max_element_width as u16);
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)))
} else {
(x, list_area.width)
};
buf.set_spans(elem_x, y + j as u16, line, max_element_width);
}
if is_selected {
buf.set_style(area, self.highlight_style);

View File

@@ -13,10 +13,13 @@
//! - [`BarChart`]
//! - [`Gauge`]
//! - [`Sparkline`]
//! - [`calendar::Monthly`]
//! - [`Clear`]
mod barchart;
mod block;
#[cfg(feature = "widget-calendar")]
pub mod calendar;
pub mod canvas;
mod chart;
mod clear;
@@ -29,14 +32,14 @@ mod table;
mod tabs;
pub use self::barchart::BarChart;
pub use self::block::{Block, BorderType};
pub use self::block::{Block, BorderType, Padding};
pub use self::chart::{Axis, Chart, Dataset, GraphType};
pub use self::clear::Clear;
pub use self::gauge::Gauge;
pub use self::gauge::{Gauge, LineGauge};
pub use self::list::{List, ListItem, ListState};
pub use self::paragraph::{Paragraph, Wrap};
pub use self::sparkline::Sparkline;
pub use self::table::{Row, Table, TableState};
pub use self::sparkline::{RenderDirection, Sparkline};
pub use self::table::{Cell, Row, Table, TableState};
pub use self::tabs::Tabs;
use crate::{buffer::Buffer, layout::Rect};
@@ -44,17 +47,17 @@ use bitflags::bitflags;
bitflags! {
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
pub struct Borders: u32 {
pub struct Borders: u8 {
/// Show no border (default)
const NONE = 0b0000_0001;
const NONE = 0b0000;
/// Show the top border
const TOP = 0b0000_0010;
const TOP = 0b0001;
/// Show the right border
const RIGHT = 0b0000_0100;
const RIGHT = 0b0010;
/// Show the bottom border
const BOTTOM = 0b000_1000;
const BOTTOM = 0b0100;
/// Show the left border
const LEFT = 0b0001_0000;
const LEFT = 0b1000;
/// Show all borders
const ALL = Self::TOP.bits | Self::RIGHT.bits | Self::BOTTOM.bits | Self::LEFT.bits;
}
@@ -62,8 +65,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);
}
@@ -86,9 +89,9 @@ pub trait Widget {
///
/// ```rust,no_run
/// # use std::io;
/// # use tui::Terminal;
/// # use tui::backend::{Backend, TermionBackend};
/// # use tui::widgets::{Widget, List, ListItem, ListState};
/// # use ratatui::Terminal;
/// # use ratatui::backend::{Backend, TestBackend};
/// # use ratatui::widgets::{Widget, List, ListItem, ListState};
///
/// // Let's say we have some events to display.
/// struct Events {
@@ -154,9 +157,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"),
@@ -166,7 +168,7 @@ pub trait Widget {
/// loop {
/// terminal.draw(|f| {
/// // The items managed by the application are transformed to something
/// // that is understood by tui.
/// // that is understood by ratatui.
/// let items: Vec<ListItem>= events.items.iter().map(|i| ListItem::new(i.as_ref())).collect();
/// // The `List` widget is then built with those items.
/// let list = List::new(items);

View File

@@ -24,10 +24,10 @@ fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment)
/// # Examples
///
/// ```
/// # use tui::text::{Text, Spans, Span};
/// # use tui::widgets::{Block, Borders, Paragraph, Wrap};
/// # use tui::style::{Style, Color, Modifier};
/// # use tui::layout::{Alignment};
/// # use ratatui::text::{Text, Spans, Span};
/// # use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
/// # use ratatui::style::{Style, Color, Modifier};
/// # use ratatui::layout::{Alignment};
/// let text = vec![
/// Spans::from(vec![
/// Span::raw("First"),
@@ -63,8 +63,8 @@ pub struct Paragraph<'a> {
/// ## Examples
///
/// ```
/// # use tui::widgets::{Paragraph, Wrap};
/// # use tui::text::Text;
/// # use ratatui::widgets::{Paragraph, Wrap};
/// # use ratatui::text::Text;
/// let bullet_points = Text::from(r#"Some indented points:
/// - First thing goes here and is long so that it wraps
/// - Here is another point that is long enough to wrap"#);
@@ -176,6 +176,10 @@ impl<'a> Widget for Paragraph<'a> {
if y >= self.scroll.0 {
let mut x = get_line_offset(current_line_width, text_area.width, self.alignment);
for StyledGrapheme { symbol, style } in current_line {
let width = symbol.width();
if width == 0 {
continue;
}
buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll.0)
.set_symbol(if symbol.is_empty() {
// If the symbol is empty, the last char which rendered last time will
@@ -185,7 +189,7 @@ impl<'a> Widget for Paragraph<'a> {
symbol
})
.set_style(*style);
x += symbol.width() as u16;
x += width as u16;
}
}
y += 1;
@@ -195,3 +199,222 @@ impl<'a> Widget for Paragraph<'a> {
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::{
style::Color,
text::{Span, Spans},
widgets::Borders,
};
#[test]
fn zero_width_char_at_end_of_line() {
let line = "foo\0";
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 1));
Paragraph::new(line).render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec!["foo"]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_empty_paragraph() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));
Paragraph::new("").render(Rect::new(0, 0, 10, 5), &mut buffer);
let expected_buffer = Buffer::with_lines(vec![" "; 5]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_single_line_paragraph() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 1));
Paragraph::new("Hello, world!").render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec!["Hello, world! "]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_multi_line_paragraph() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
Paragraph::new("This is a\nmultiline\nparagraph.").render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec![
"This is a ",
"multiline ",
"paragraph. ",
]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_block() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
Paragraph::new("Hello, world!")
.block(Block::default().title("Title").borders(Borders::ALL))
.render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec![
"┌Title────────┐",
"│Hello, world!│",
"└─────────────┘",
]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_without_block() {
let area = Rect::new(0, 0, 15, 1);
let mut buffer = Buffer::empty(area);
Paragraph::new("Hello, world!").render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec!["Hello, world! "]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_word_wrap() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 4));
Paragraph::new("This is a long line of text that should wrap.")
.wrap(Wrap { trim: true })
.render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec![
"This is a long ",
"line of text ",
"that should ",
"wrap. ",
]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_line_truncation() {
let paragraph = Paragraph::new("This is a long line of text that should be truncated.");
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
paragraph.render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec!["This is a "]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_left_alignment() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 1));
Paragraph::new("Hello, world!")
.alignment(Alignment::Left)
.render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec!["Hello, world! "]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_center_alignment() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 1));
Paragraph::new("Hello, world!")
.alignment(Alignment::Center)
.render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec![" Hello, world! "]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_right_alignment() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 1));
Paragraph::new("Hello, world!")
.alignment(Alignment::Right)
.render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec![" Hello, world!"]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_scroll_offset() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
Paragraph::new("This is a\nmultiline\nparagraph.")
.scroll((1, 0))
.render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec![
"multiline ",
"paragraph. ",
" ",
]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_zero_width_area() {
let area = Rect::new(0, 0, 0, 3);
let mut buffer = Buffer::empty(area);
Paragraph::new("Hello, world!").render(buffer.area, &mut buffer);
let expected_buffer = Buffer::empty(area);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_zero_height_area() {
let area = Rect::new(0, 0, 10, 0);
let mut buffer = Buffer::empty(area);
Paragraph::new("Hello, world!").render(buffer.area, &mut buffer);
let expected_buffer = Buffer::empty(area);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_styled_text() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 13, 1));
Paragraph::new(Spans::from(vec![
Span::styled("Hello, ", Style::default().fg(Color::Red)),
Span::styled("world!", Style::default().fg(Color::Blue)),
]))
.render(buffer.area, &mut buffer);
let mut expected_buffer = Buffer::with_lines(vec!["Hello, world!"]);
expected_buffer.set_style(Rect::new(0, 0, 7, 1), Style::default().fg(Color::Red));
expected_buffer.set_style(Rect::new(7, 0, 6, 1), Style::default().fg(Color::Blue));
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_special_characters() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 1));
Paragraph::new("Hello, <world>!").render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec!["Hello, <world>!"]);
assert_eq!(buffer, expected_buffer);
}
#[test]
fn test_render_paragraph_with_unicode_characters() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 20, 1));
Paragraph::new("こんにちは, 世界! 😃").render(buffer.area, &mut buffer);
let expected_buffer = Buffer::with_lines(vec!["こんにちは, 世界! 😃"]);
assert_eq!(buffer, expected_buffer);
}
}

View File

@@ -57,7 +57,7 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
let mut symbols_exhausted = true;
for StyledGrapheme { symbol, style } in &mut self.symbols {
symbols_exhausted = false;
let symbol_whitespace = symbol.chars().all(&char::is_whitespace);
let symbol_whitespace = symbol.chars().all(&char::is_whitespace) && symbol != NBSP;
// Ignore characters wider that the total max width.
if symbol.width() as u16 > self.max_line_width
@@ -77,7 +77,7 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
}
// Mark the previous symbol as word end.
if symbol_whitespace && !prev_whitespace && symbol != NBSP {
if symbol_whitespace && !prev_whitespace {
symbols_to_last_word_end = self.current_line.len();
width_to_last_word_end = current_line_width;
}
@@ -127,7 +127,7 @@ pub struct LineTruncator<'a, 'b> {
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
max_line_width: u16,
current_line: Vec<StyledGrapheme<'a>>,
/// Record the offet to skip render
/// Record the offset to skip render
horizontal_offset: u16,
}
@@ -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![
"コンピュータ上で文字",
@@ -438,7 +438,7 @@ mod test {
assert_eq!(line_truncator, vec![" "]);
}
/// Tests an input starting with a letter, folowed by spaces - some of the behaviour is
/// Tests an input starting with a letter, followed by spaces - some of the behaviour is
/// incidental.
#[test]
fn line_composer_char_plus_lots_of_spaces() {
@@ -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

@@ -12,8 +12,8 @@ use std::cmp::min;
/// # Examples
///
/// ```
/// # use tui::widgets::{Block, Borders, Sparkline};
/// # use tui::style::{Style, Color};
/// # use ratatui::widgets::{Block, Borders, Sparkline};
/// # use ratatui::style::{Style, Color};
/// Sparkline::default()
/// .block(Block::default().title("Sparkline").borders(Borders::ALL))
/// .data(&[0, 2, 3, 4, 1, 4, 10])
@@ -31,8 +31,18 @@ pub struct Sparkline<'a> {
/// The maximum value to take to compute the maximum bar height (if nothing is specified, the
/// widget uses the max of the dataset)
max: Option<u64>,
/// If true, draws a baseline of `bar::ONE_EIGHTH` spanning the bottom of the sparkline graph
show_baseline: bool,
/// A set of bar symbols used to represent the give data
bar_set: symbols::bar::Set,
// The direction to render the sparkine, either from left to right, or from right to left
direction: RenderDirection,
}
#[derive(Debug, Clone, Copy)]
pub enum RenderDirection {
LeftToRight,
RightToLeft,
}
impl<'a> Default for Sparkline<'a> {
@@ -42,7 +52,9 @@ impl<'a> Default for Sparkline<'a> {
style: Default::default(),
data: &[],
max: None,
show_baseline: false,
bar_set: symbols::bar::NINE_LEVELS,
direction: RenderDirection::LeftToRight,
}
}
}
@@ -68,10 +80,20 @@ impl<'a> Sparkline<'a> {
self
}
pub fn show_baseline(mut self, show_baseline: bool) -> Sparkline<'a> {
self.show_baseline = show_baseline;
self
}
pub fn bar_set(mut self, bar_set: symbols::bar::Set) -> Sparkline<'a> {
self.bar_set = bar_set;
self
}
pub fn direction(mut self, direction: RenderDirection) -> Sparkline<'a> {
self.direction = direction;
self
}
}
impl<'a> Widget for Sparkline<'a> {
@@ -89,6 +111,14 @@ impl<'a> Widget for Sparkline<'a> {
return;
}
if self.show_baseline {
for i in spark_area.left()..spark_area.right() {
buf.get_mut(i, spark_area.bottom() - 1)
.set_symbol(self.bar_set.one_eighth)
.set_style(self.style);
}
}
let max = match self.max {
Some(v) => v,
None => *self.data.iter().max().unwrap_or(&1u64),
@@ -109,7 +139,13 @@ impl<'a> Widget for Sparkline<'a> {
for j in (0..spark_area.height).rev() {
for (i, d) in data.iter_mut().enumerate() {
let symbol = match *d {
0 => self.bar_set.empty,
0 => {
if self.show_baseline && j == spark_area.height - 1 {
self.bar_set.one_eighth
} else {
self.bar_set.empty
}
}
1 => self.bar_set.one_eighth,
2 => self.bar_set.one_quarter,
3 => self.bar_set.three_eighths,
@@ -119,7 +155,11 @@ impl<'a> Widget for Sparkline<'a> {
7 => self.bar_set.seven_eighths,
_ => self.bar_set.full,
};
buf.get_mut(spark_area.left() + i as u16, spark_area.top() + j)
let x = match self.direction {
RenderDirection::LeftToRight => spark_area.left() + i as u16,
RenderDirection::RightToLeft => spark_area.right() - i as u16 - 1,
};
buf.get_mut(x, spark_area.top() + j)
.set_symbol(symbol)
.set_style(self.style);
@@ -136,20 +176,126 @@ impl<'a> Widget for Sparkline<'a> {
#[cfg(test)]
mod tests {
use super::*;
use crate::{assert_buffer_eq, buffer::Cell};
// Helper function to render a sparkline to a buffer with a given width
// filled with x symbols to make it easier to assert on the result
fn render(widget: Sparkline, width: u16, height: u16) -> Buffer {
let mut cell = Cell::default();
cell.set_symbol("x");
let mut buffer = Buffer::filled(Rect::new(0, 0, width, height), &cell);
widget.render(buffer.area, &mut buffer);
buffer
}
#[test]
fn it_does_not_panic_if_max_is_zero() {
let widget = Sparkline::default().data(&[0, 0, 0]);
let area = Rect::new(0, 0, 3, 1);
let mut buffer = Buffer::empty(area);
widget.render(area, &mut buffer);
let buffer = render(widget, 6, 1);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" xxx"]));
}
#[test]
fn it_does_not_panic_if_max_is_set_to_zero() {
let widget = Sparkline::default().data(&[0, 1, 2]).max(0);
let area = Rect::new(0, 0, 3, 1);
let mut buffer = Buffer::empty(area);
widget.render(area, &mut buffer);
let buffer = render(widget, 6, 1);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" xxx"]));
}
#[test]
fn it_renders() {
let widget = Sparkline::default().data(&[0, 1, 2, 3, 4, 5, 6, 7, 8]);
let buffer = render(widget, 12, 1);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▁▂▃▄▅▆▇█xxx"]));
}
#[test]
fn it_renders_with_max_more_than_data_max() {
let widget = Sparkline::default()
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
.max(16);
let buffer = render(widget, 12, 1);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▁▁▂▂▃▃▄xxx"]));
}
#[test]
fn it_renders_with_max_less_than_data_max() {
let widget = Sparkline::default()
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
.max(4);
let buffer = render(widget, 12, 1);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▂▄▆█████xxx"]));
}
#[test]
fn it_renders_with_multi_line() {
let widget = Sparkline::default().data(&[0, 1, 2, 3, 4, 5, 6, 7, 8]);
let buffer = render(widget, 15, 3);
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
" ▂▅█xxxxxx",
" ▁▄▇███xxxxxx",
" ▃▆██████xxxxxx",
])
);
}
#[test]
fn it_renders_with_multi_line_and_baseline() {
let widget = Sparkline::default()
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
.show_baseline(true);
let buffer = render(widget, 15, 3);
assert_buffer_eq!(
buffer,
// this currently fails because the baseline logic doesn't clear
// the parts above the line
// " ▂▅█xxxxxx",
// " ▁▄▇███xxxxxx",
// " ▃▆██████▁▁▁▁▁▁",
Buffer::with_lines(vec![
" ▂▅█ ",
" ▁▄▇███ ",
" ▃▆██████▁▁▁▁▁▁",
])
);
}
#[test]
fn it_renders_left_to_right() {
let widget = Sparkline::default()
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
.direction(RenderDirection::LeftToRight);
let buffer = render(widget, 12, 1);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▁▂▃▄▅▆▇█xxx"]));
}
#[test]
fn it_renders_right_to_left() {
let widget = Sparkline::default()
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
.direction(RenderDirection::RightToLeft);
let buffer = render(widget, 12, 1);
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["xxx█▇▆▅▄▃▂▁ "]));
}
#[test]
fn it_renders_baseline() {
let widget = Sparkline::default()
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
.show_baseline(true);
let buffer = render(widget, 12, 1);
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["▁▁▂▃▄▅▆▇█▁▁▁"]));
}
#[test]
fn it_renders_baseline_right_to_left() {
let widget = Sparkline::default()
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
.direction(RenderDirection::RightToLeft)
.show_baseline(true);
let buffer = render(widget, 12, 1);
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["▁▁▁█▇▆▅▄▃▂▁▁"]));
}
}

View File

@@ -1,36 +1,347 @@
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,
fmt::Display,
iter::{self, Iterator},
};
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone)]
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
///
/// It can be created from anything that can be converted to a [`Text`].
/// ```rust
/// # use ratatui::widgets::Cell;
/// # use ratatui::style::{Style, Modifier};
/// # use ratatui::text::{Span, Spans, Text};
/// # use std::borrow::Cow;
/// Cell::from("simple string");
///
/// Cell::from(Span::from("span"));
///
/// Cell::from(Spans::from(vec![
/// Span::raw("a vec of "),
/// Span::styled("spans", Style::default().add_modifier(Modifier::BOLD))
/// ]));
///
/// 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
/// capabilities of [`Text`].
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Cell<'a> {
content: Text<'a>,
style: Style,
}
impl<'a> Cell<'a> {
/// Set the `Style` of this cell.
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
}
impl<'a, T> From<T> for Cell<'a>
where
T: Into<Text<'a>>,
{
fn from(content: T) -> Cell<'a> {
Cell {
content: content.into(),
style: Style::default(),
}
}
}
/// Holds data to be displayed in a [`Table`] widget.
///
/// A [`Row`] is a collection of cells. It can be created from simple strings:
/// ```rust
/// # use ratatui::widgets::Row;
/// Row::new(vec!["Cell1", "Cell2", "Cell3"]);
/// ```
///
/// But if you need a bit more control over individual cells, you can explicitly create [`Cell`]s:
/// ```rust
/// # use ratatui::widgets::{Row, Cell};
/// # use ratatui::style::{Style, Color};
/// Row::new(vec![
/// Cell::from("Cell1"),
/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
/// ]);
/// ```
///
/// You can also construct a row from any type that can be converted into [`Text`]:
/// ```rust
/// # use std::borrow::Cow;
/// # use ratatui::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, Eq, Default)]
pub struct Row<'a> {
cells: Vec<Cell<'a>>,
height: u16,
style: Style,
bottom_margin: u16,
}
impl<'a> Row<'a> {
/// Creates a new [`Row`] from an iterator where items can be converted to a [`Cell`].
pub fn new<T>(cells: T) -> Self
where
T: IntoIterator,
T::Item: Into<Cell<'a>>,
{
Self {
height: 1,
cells: cells.into_iter().map(|c| c.into()).collect(),
style: Style::default(),
bottom_margin: 0,
}
}
/// Set the fixed height of the [`Row`]. Any [`Cell`] whose content has more lines than this
/// height will see its content truncated.
pub fn height(mut self, height: u16) -> Self {
self.height = height;
self
}
/// Set the [`Style`] of the entire row. This [`Style`] can be overridden by the [`Style`] of a
/// any individual [`Cell`] or event by their [`Text`] content.
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
/// Set the bottom margin. By default, the bottom margin is `0`.
pub fn bottom_margin(mut self, margin: u16) -> Self {
self.bottom_margin = margin;
self
}
/// Returns the total height of the row.
fn total_height(&self) -> u16 {
self.height.saturating_add(self.bottom_margin)
}
}
/// A widget to display data in formatted columns.
///
/// It is a collection of [`Row`]s, themselves composed of [`Cell`]s:
/// ```rust
/// # use ratatui::widgets::{Block, Borders, Table, Row, Cell};
/// # use ratatui::layout::Constraint;
/// # use ratatui::style::{Style, Color, Modifier};
/// # use ratatui::text::{Text, Spans, Span};
/// Table::new(vec![
/// // Row can be created from simple strings.
/// Row::new(vec!["Row11", "Row12", "Row13"]),
/// // You can style the entire row.
/// Row::new(vec!["Row21", "Row22", "Row23"]).style(Style::default().fg(Color::Blue)),
/// // If you need more control over the styling you may need to create Cells directly
/// Row::new(vec![
/// Cell::from("Row31"),
/// Cell::from("Row32").style(Style::default().fg(Color::Yellow)),
/// Cell::from(Spans::from(vec![
/// Span::raw("Row"),
/// Span::styled("33", Style::default().fg(Color::Green))
/// ])),
/// ]),
/// // If a Row need to display some content over multiple lines, you just have to change
/// // its height.
/// Row::new(vec![
/// Cell::from("Row\n41"),
/// Cell::from("Row\n42"),
/// Cell::from("Row\n43"),
/// ]).height(2),
/// ])
/// // You can set the style of the entire Table.
/// .style(Style::default().fg(Color::White))
/// // It has an optional header, which is simply a Row always visible at the top.
/// .header(
/// Row::new(vec!["Col1", "Col2", "Col3"])
/// .style(Style::default().fg(Color::Yellow))
/// // If you want some space between the header and the rest of the rows, you can always
/// // specify some margin at the bottom.
/// .bottom_margin(1)
/// )
/// // As any other widget, a Table can be wrapped in a Block.
/// .block(Block::default().title("Table"))
/// // Columns widths are constrained in the same way as Layout...
/// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)])
/// // ...and they can be separated by a fixed spacing.
/// .column_spacing(1)
/// // If you wish to highlight a row in any specific way when it is selected...
/// .highlight_style(Style::default().add_modifier(Modifier::BOLD))
/// // ...and potentially show a symbol in front of the selection.
/// .highlight_symbol(">>");
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Table<'a> {
/// A block to wrap the widget in
block: Option<Block<'a>>,
/// Base style for the widget
style: Style,
/// Width constraints for each column
widths: &'a [Constraint],
/// Space between each column
column_spacing: u16,
/// Style used to render the selected row
highlight_style: Style,
/// Symbol in front of the selected rom
highlight_symbol: Option<&'a str>,
/// Optional header
header: Option<Row<'a>>,
/// Data to display in each row
rows: Vec<Row<'a>>,
}
impl<'a> Table<'a> {
pub fn new<T>(rows: T) -> Self
where
T: IntoIterator<Item = Row<'a>>,
{
Self {
block: None,
style: Style::default(),
widths: &[],
column_spacing: 1,
highlight_style: Style::default(),
highlight_symbol: None,
header: None,
rows: rows.into_iter().collect(),
}
}
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
pub fn header(mut self, header: Row<'a>) -> Self {
self.header = Some(header);
self
}
pub fn widths(mut self, widths: &'a [Constraint]) -> Self {
let between_0_and_100 = |&w| match w {
Constraint::Percentage(p) => p <= 100,
_ => true,
};
assert!(
widths.iter().all(between_0_and_100),
"Percentages should be between 0 and 100 inclusively."
);
self.widths = widths;
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Self {
self.highlight_symbol = Some(highlight_symbol);
self
}
pub fn highlight_style(mut self, highlight_style: Style) -> Self {
self.highlight_style = highlight_style;
self
}
pub fn column_spacing(mut self, spacing: u16) -> Self {
self.column_spacing = spacing;
self
}
fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec<u16> {
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);
constraints.push(Constraint::Length(highlight_symbol_width));
}
for constraint in self.widths {
constraints.push(*constraint);
constraints.push(Constraint::Length(self.column_spacing));
}
if !self.widths.is_empty() {
constraints.pop();
}
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.expand_to_fill(false)
.split(Rect {
x: 0,
y: 0,
width: max_width,
height: 1,
});
let mut chunks = &chunks[..];
if has_selection {
chunks = &chunks[1..];
}
chunks.iter().step_by(2).map(|c| c.width).collect()
}
fn get_row_bounds(
&self,
selected: Option<usize>,
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;
for item in self.rows.iter().skip(offset) {
if height + item.height > max_height {
break;
}
height += item.total_height();
end += 1;
}
let selected = selected.unwrap_or(0).min(self.rows.len() - 1);
while selected >= end {
height = height.saturating_add(self.rows[end].total_height());
end += 1;
while height > max_height {
height = height.saturating_sub(self.rows[start].total_height());
start += 1;
}
}
while selected < start {
start -= 1;
height = height.saturating_add(self.rows[start].total_height());
while height > max_height {
end -= 1;
height = height.saturating_sub(self.rows[end].total_height());
}
}
(start, end)
}
}
#[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
@@ -42,187 +353,23 @@ impl TableState {
self.offset = 0;
}
}
}
/// Holds data to be displayed in a Table widget
#[derive(Debug, Clone)]
pub enum Row<D>
where
D: Iterator,
D::Item: Display,
{
Data(D),
StyledData(D, Style),
}
/// A widget to display data in formatted columns
///
/// # Examples
///
/// ```
/// # use tui::widgets::{Block, Borders, Table, Row};
/// # use tui::layout::Constraint;
/// # use tui::style::{Style, Color};
/// let row_style = Style::default().fg(Color::White);
/// Table::new(
/// ["Col1", "Col2", "Col3"].into_iter(),
/// vec![
/// Row::StyledData(["Row11", "Row12", "Row13"].into_iter(), row_style),
/// Row::StyledData(["Row21", "Row22", "Row23"].into_iter(), row_style),
/// Row::StyledData(["Row31", "Row32", "Row33"].into_iter(), row_style),
/// Row::Data(["Row41", "Row42", "Row43"].into_iter())
/// ].into_iter()
/// )
/// .block(Block::default().title("Table"))
/// .header_style(Style::default().fg(Color::Yellow))
/// .widths(&[Constraint::Length(5), Constraint::Length(5), Constraint::Length(10)])
/// .style(Style::default().fg(Color::White))
/// .column_spacing(1);
/// ```
#[derive(Debug, Clone)]
pub struct Table<'a, H, R> {
/// A block to wrap the widget in
block: Option<Block<'a>>,
/// Base style for the widget
style: Style,
/// Header row for all columns
header: H,
/// Style for the header
header_style: Style,
/// Width constraints for each column
widths: &'a [Constraint],
/// Space between each column
column_spacing: u16,
/// Space between the header and the rows
header_gap: u16,
/// Style used to render the selected row
highlight_style: Style,
/// Symbol in front of the selected rom
highlight_symbol: Option<&'a str>,
/// Data to display in each row
rows: R,
}
impl<'a, H, R> Default for Table<'a, H, R>
where
H: Iterator + Default,
R: Iterator + Default,
{
fn default() -> Table<'a, H, R> {
Table {
block: None,
style: Style::default(),
header: H::default(),
header_style: Style::default(),
widths: &[],
column_spacing: 1,
header_gap: 1,
highlight_style: Style::default(),
highlight_symbol: None,
rows: R::default(),
}
}
}
impl<'a, H, D, R> Table<'a, H, R>
where
H: Iterator,
D: Iterator,
D::Item: Display,
R: Iterator<Item = Row<D>>,
{
pub fn new(header: H, rows: R) -> Table<'a, H, R> {
Table {
block: None,
style: Style::default(),
header,
header_style: Style::default(),
widths: &[],
column_spacing: 1,
header_gap: 1,
highlight_style: Style::default(),
highlight_symbol: None,
rows,
}
}
pub fn block(mut self, block: Block<'a>) -> Table<'a, H, R> {
self.block = Some(block);
self
}
pub fn header<II>(mut self, header: II) -> Table<'a, H, R>
where
II: IntoIterator<Item = H::Item, IntoIter = H>,
{
self.header = header.into_iter();
self
}
pub fn header_style(mut self, style: Style) -> Table<'a, H, R> {
self.header_style = style;
self
}
pub fn widths(mut self, widths: &'a [Constraint]) -> Table<'a, H, R> {
let between_0_and_100 = |&w| match w {
Constraint::Percentage(p) => p <= 100,
_ => true,
};
assert!(
widths.iter().all(between_0_and_100),
"Percentages should be between 0 and 100 inclusively."
);
self.widths = widths;
self
}
pub fn rows<II>(mut self, rows: II) -> Table<'a, H, R>
where
II: IntoIterator<Item = Row<D>, IntoIter = R>,
{
self.rows = rows.into_iter();
self
}
pub fn style(mut self, style: Style) -> Table<'a, H, R> {
self.style = style;
self
}
pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Table<'a, H, R> {
self.highlight_symbol = Some(highlight_symbol);
self
}
pub fn highlight_style(mut self, highlight_style: Style) -> Table<'a, H, R> {
self.highlight_style = highlight_style;
self
}
pub fn column_spacing(mut self, spacing: u16) -> Table<'a, H, R> {
self.column_spacing = spacing;
self
}
pub fn header_gap(mut self, gap: u16) -> Table<'a, H, R> {
self.header_gap = gap;
self
/// Returns a copy of the receiver's scroll offset.
///
/// This is useful, for example, if you need to "synchronize" the scrolling of a `Table` and a `Paragraph`.
pub fn offset(&self) -> usize {
self.offset
}
}
impl<'a, H, D, R> StatefulWidget for Table<'a, H, R>
where
H: Iterator,
H::Item: Display,
D: Iterator,
D::Item: Display,
R: Iterator<Item = Row<D>>,
{
impl<'a> StatefulWidget for Table<'a> {
type State = TableState;
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
if area.area() == 0 {
return;
}
buf.set_style(area, self.style);
// Render block if necessary and get the drawing area
let table_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
@@ -232,124 +379,113 @@ where
None => area,
};
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);
}
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 * area.width) / 100.0)
}
Constraint::Ratio(n, d) => {
variables[i] | EQ(WEAK) | (f64::from(area.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),
})
}
solver
.add_constraint(
variables
.iter()
.fold(Expression::from_constant(0.), |acc, v| acc + *v)
| LE(REQUIRED)
| f64::from(
area.width - 2 - (self.column_spacing * (variables.len() as u16 - 1)),
),
)
.unwrap();
solver.add_constraints(&ccs).unwrap();
let mut solved_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 as u16
};
solved_widths[index] = value
}
let mut y = table_area.top();
let mut x = table_area.left();
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 = " ".repeat(highlight_symbol.width());
let mut current_height = 0;
let mut rows_height = table_area.height;
// Draw header
if y < table_area.bottom() {
for (w, t) in solved_widths.iter().zip(self.header.by_ref()) {
buf.set_stringn(x, y, format!("{}", t), *w as usize, self.header_style);
x += *w + self.column_spacing;
if let Some(ref header) = self.header {
let max_header_height = table_area.height.min(header.total_height());
buf.set_style(
Rect {
x: table_area.left(),
y: table_area.top(),
width: table_area.width,
height: table_area.height.min(header.height),
},
header.style,
);
let mut col = table_area.left();
if has_selection {
col += (highlight_symbol.width() as u16).min(table_area.width);
}
for (width, cell) in columns_widths.iter().zip(header.cells.iter()) {
render_cell(
buf,
cell,
Rect {
x: col,
y: table_area.top(),
width: *width,
height: max_header_height,
},
);
col += *width + self.column_spacing;
}
current_height += max_header_height;
rows_height = rows_height.saturating_sub(max_header_height);
}
y += 1 + self.header_gap;
// Use highlight_style only if something is selected
let (selected, highlight_style) = match state.selected {
Some(i) => (Some(i), self.highlight_style),
None => (None, self.style),
};
let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = iter::repeat(" ")
.take(highlight_symbol.width())
.collect::<String>();
// Draw rows
let default_style = Style::default();
if y < table_area.bottom() {
let remaining = (table_area.bottom() - y) as usize;
// Make sure the table shows the selected item
state.offset = if let Some(selected) = selected {
if selected >= remaining + state.offset - 1 {
selected + 1 - remaining
} else if selected < state.offset {
selected
} else {
state.offset
}
} else {
0
if self.rows.is_empty() {
return;
}
let (start, end) = self.get_row_bounds(state.selected, state.offset, rows_height);
state.offset = start;
for (i, table_row) in self
.rows
.iter_mut()
.enumerate()
.skip(state.offset)
.take(end - start)
{
let (row, col) = (table_area.top() + current_height, table_area.left());
current_height += table_row.total_height();
let table_row_area = Rect {
x: col,
y: row,
width: table_area.width,
height: table_row.height,
};
for (i, row) in self.rows.skip(state.offset).take(remaining).enumerate() {
let (data, style, symbol) = match row {
Row::Data(d) | Row::StyledData(d, _)
if Some(i) == state.selected.map(|s| s - state.offset) =>
{
(d, highlight_style, highlight_symbol)
}
Row::Data(d) => (d, default_style, blank_symbol.as_ref()),
Row::StyledData(d, s) => (d, s, blank_symbol.as_ref()),
buf.set_style(table_row_area, table_row.style);
let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
let table_row_start_col = if has_selection {
let symbol = if is_selected {
highlight_symbol
} else {
&blank_symbol
};
x = table_area.left();
for (c, (w, elt)) in solved_widths.iter().zip(data).enumerate() {
let s = if c == 0 {
format!("{}{}", symbol, elt)
} else {
format!("{}", elt)
};
buf.set_stringn(x, y + i as u16, s, *w as usize, style);
x += *w + self.column_spacing;
}
let (col, _) =
buf.set_stringn(col, row, symbol, table_area.width as usize, table_row.style);
col
} else {
col
};
let mut col = table_row_start_col;
for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) {
render_cell(
buf,
cell,
Rect {
x: col,
y: row,
width: *width,
height: table_row.height,
},
);
col += *width + self.column_spacing;
}
if is_selected {
buf.set_style(table_row_area, self.highlight_style);
}
}
}
}
impl<'a, H, D, R> Widget for Table<'a, H, R>
where
H: Iterator,
H::Item: Display,
D: Iterator,
D::Item: Display,
R: Iterator<Item = Row<D>>,
{
fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) {
buf.set_style(area, cell.style);
for (i, spans) in cell.content.lines.iter().enumerate() {
if i as u16 >= area.height {
break;
}
buf.set_spans(area.x, area.y + i as u16, spans, area.width);
}
}
impl<'a> Widget for Table<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default();
StatefulWidget::render(self, area, buf, &mut state);
@@ -363,7 +499,6 @@ mod tests {
#[test]
#[should_panic]
fn table_invalid_percentages() {
Table::new([""].iter(), vec![Row::Data([""].iter())].into_iter())
.widths(&[Constraint::Percentage(110)]);
Table::new(vec![]).widths(&[Constraint::Percentage(110)]);
}
}

View File

@@ -12,10 +12,10 @@ use crate::{
/// # Examples
///
/// ```
/// # use tui::widgets::{Block, Borders, Tabs};
/// # use tui::style::{Style, Color};
/// # use tui::text::{Spans};
/// # use tui::symbols::{DOT};
/// # use ratatui::widgets::{Block, Borders, Tabs};
/// # use ratatui::style::{Style, Color};
/// # use ratatui::text::{Spans};
/// # use ratatui::symbols::{DOT};
/// let titles = ["Tab1", "Tab2", "Tab3", "Tab4"].iter().cloned().map(Spans::from).collect();
/// Tabs::new(titles)
/// .block(Block::default().title("Tabs").borders(Borders::ALL))

View File

@@ -6,7 +6,7 @@ fn backend_termion_should_only_write_diffs() -> Result<(), Box<dyn std::error::E
let mut bytes = Vec::new();
let mut stdout = Cursor::new(&mut bytes);
{
use tui::{
use ratatui::{
backend::TermionBackend, layout::Rect, widgets::Paragraph, Terminal, TerminalOptions,
Viewport,
};
@@ -15,7 +15,7 @@ fn backend_termion_should_only_write_diffs() -> Result<(), Box<dyn std::error::E
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::fixed(area),
viewport: Viewport::Fixed(area),
},
)?;
terminal.draw(|f| {
@@ -34,21 +34,21 @@ fn backend_termion_should_only_write_diffs() -> Result<(), Box<dyn std::error::E
let mut s = String::new();
// First draw
write!(s, "{}", cursor::Goto(1, 1))?;
s.push_str("a");
s.push('a');
write!(s, "{}", color::Fg(color::Reset))?;
write!(s, "{}", color::Bg(color::Reset))?;
write!(s, "{}", style::Reset)?;
write!(s, "{}", cursor::Hide)?;
// Second draw
write!(s, "{}", cursor::Goto(2, 1))?;
s.push_str("b");
s.push('b');
write!(s, "{}", color::Fg(color::Reset))?;
write!(s, "{}", color::Bg(color::Reset))?;
write!(s, "{}", style::Reset)?;
write!(s, "{}", cursor::Hide)?;
// Third draw
write!(s, "{}", cursor::Goto(3, 1))?;
s.push_str("c");
s.push('c');
write!(s, "{}", color::Fg(color::Reset))?;
write!(s, "{}", color::Bg(color::Reset))?;
write!(s, "{}", style::Reset)?;

View File

@@ -1,7 +1,10 @@
use tui::{
use ratatui::{
backend::{Backend, TestBackend},
layout::Rect,
widgets::Paragraph,
Terminal,
};
use std::error::Error;
#[test]
fn terminal_buffer_size_should_be_limited() {
@@ -11,3 +14,23 @@ fn terminal_buffer_size_should_be_limited() {
assert_eq!(size.width, 255);
assert_eq!(size.height, 255);
}
#[test]
fn terminal_draw_returns_the_completed_frame() -> Result<(), Box<dyn Error>> {
let backend = TestBackend::new(10, 10);
let mut terminal = Terminal::new(backend)?;
let frame = terminal.draw(|f| {
let paragraph = Paragraph::new("Test");
f.render_widget(paragraph, f.size());
})?;
assert_eq!(frame.buffer.get(0, 0).symbol, "T");
assert_eq!(frame.area, Rect::new(0, 0, 10, 10));
terminal.backend_mut().resize(8, 8);
let frame = terminal.draw(|f| {
let paragraph = Paragraph::new("test");
f.render_widget(paragraph, f.size());
})?;
assert_eq!(frame.buffer.get(0, 0).symbol, "t");
assert_eq!(frame.area, Rect::new(0, 0, 8, 8));
Ok(())
}

40
tests/widgets_barchart.rs Normal file
View File

@@ -0,0 +1,40 @@
use ratatui::backend::TestBackend;
use ratatui::buffer::Buffer;
use ratatui::widgets::{BarChart, Block, Borders};
use ratatui::Terminal;
#[test]
fn widgets_barchart_not_full_below_max_value() {
let test_case = |expected| {
let backend = TestBackend::new(30, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let barchart = BarChart::default()
.block(Block::default().borders(Borders::ALL))
.data(&[("empty", 0), ("half", 50), ("almost", 99), ("full", 100)])
.max(100)
.bar_width(7)
.bar_gap(0);
f.render_widget(barchart, size);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
// check that bars fill up correctly up to max value
test_case(Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ ▇▇▇▇▇▇▇███████│",
"│ ██████████████│",
"│ ██████████████│",
"│ ▄▄▄▄▄▄▄██████████████│",
"│ █████████████████████│",
"│ █████████████████████│",
"│ ██50█████99█████100██│",
"│empty half almost full │",
"└────────────────────────────┘",
]));
}

View File

@@ -1,7 +1,8 @@
use tui::{
use ratatui::{
assert_buffer_eq,
backend::TestBackend,
buffer::Buffer,
layout::Rect,
layout::{Alignment, Rect},
style::{Color, Style},
text::Span,
widgets::{Block, Borders},
@@ -45,3 +46,438 @@ fn widgets_block_renders() {
}
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_block_renders_on_small_areas() {
let test_case = |block, area: Rect, expected| {
let backend = TestBackend::new(area.width, area.height);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
f.render_widget(block, area);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
let one_cell_test_cases = [
(Borders::NONE, "T"),
(Borders::LEFT, ""),
(Borders::TOP, "T"),
(Borders::RIGHT, ""),
(Borders::BOTTOM, "T"),
(Borders::ALL, ""),
];
for (borders, symbol) in one_cell_test_cases.iter().cloned() {
test_case(
Block::default().title("Test").borders(borders),
Rect {
x: 0,
y: 0,
width: 0,
height: 0,
},
Buffer::empty(Rect {
x: 0,
y: 0,
width: 0,
height: 0,
}),
);
test_case(
Block::default().title("Test").borders(borders),
Rect {
x: 0,
y: 0,
width: 1,
height: 0,
},
Buffer::empty(Rect {
x: 0,
y: 0,
width: 1,
height: 0,
}),
);
test_case(
Block::default().title("Test").borders(borders),
Rect {
x: 0,
y: 0,
width: 0,
height: 1,
},
Buffer::empty(Rect {
x: 0,
y: 0,
width: 0,
height: 1,
}),
);
test_case(
Block::default().title("Test").borders(borders),
Rect {
x: 0,
y: 0,
width: 1,
height: 1,
},
Buffer::with_lines(vec![symbol]),
);
}
test_case(
Block::default().title("Test").borders(Borders::LEFT),
Rect {
x: 0,
y: 0,
width: 4,
height: 1,
},
Buffer::with_lines(vec!["│Tes"]),
);
test_case(
Block::default().title("Test").borders(Borders::RIGHT),
Rect {
x: 0,
y: 0,
width: 4,
height: 1,
},
Buffer::with_lines(vec!["Tes│"]),
);
test_case(
Block::default().title("Test").borders(Borders::RIGHT),
Rect {
x: 0,
y: 0,
width: 4,
height: 1,
},
Buffer::with_lines(vec!["Tes│"]),
);
test_case(
Block::default()
.title("Test")
.borders(Borders::LEFT | Borders::RIGHT),
Rect {
x: 0,
y: 0,
width: 4,
height: 1,
},
Buffer::with_lines(vec!["│Te│"]),
);
test_case(
Block::default().title("Test").borders(Borders::TOP),
Rect {
x: 0,
y: 0,
width: 4,
height: 1,
},
Buffer::with_lines(vec!["Test"]),
);
test_case(
Block::default().title("Test").borders(Borders::TOP),
Rect {
x: 0,
y: 0,
width: 5,
height: 1,
},
Buffer::with_lines(vec!["Test─"]),
);
test_case(
Block::default()
.title("Test")
.borders(Borders::LEFT | Borders::TOP),
Rect {
x: 0,
y: 0,
width: 5,
height: 1,
},
Buffer::with_lines(vec!["┌Test"]),
);
test_case(
Block::default()
.title("Test")
.borders(Borders::LEFT | Borders::TOP),
Rect {
x: 0,
y: 0,
width: 6,
height: 1,
},
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);
let area = Rect::new(1, 0, 13, 2);
assert_buffer_eq!(Buffer::empty(area), Buffer::empty(area));
};
// 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 ", " "]),
);
}
#[test]
fn widgets_block_title_alignment_bottom() {
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)
.title_on_bottom()
.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 bottom-left with all borders
test_case(
Alignment::Left,
Borders::ALL,
Buffer::with_lines(vec![" ┌───────────┐ ", " └Title──────┘ "]),
);
// title bottom-left without bottom border
test_case(
Alignment::Left,
Borders::LEFT | Borders::TOP | Borders::RIGHT,
Buffer::with_lines(vec![" ┌───────────┐ ", " │Title │ "]),
);
// title bottom-left with no left border
test_case(
Alignment::Left,
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
Buffer::with_lines(vec![" ────────────┐ ", " Title───────┘ "]),
);
// title bottom-left without right border
test_case(
Alignment::Left,
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
Buffer::with_lines(vec![" ┌──────────── ", " └Title─────── "]),
);
// title bottom-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 bottom border
test_case(
Alignment::Center,
Borders::LEFT | Borders::TOP | 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 bottom-right with all borders
test_case(
Alignment::Right,
Borders::ALL,
Buffer::with_lines(vec![" ┌───────────┐ ", " └──────Title┘ "]),
);
// title bottom-right without bottom border
test_case(
Alignment::Right,
Borders::LEFT | Borders::TOP | Borders::RIGHT,
Buffer::with_lines(vec![" ┌───────────┐ ", " │ Title│ "]),
);
// title bottom-right with no left border
test_case(
Alignment::Right,
Borders::TOP | Borders::RIGHT | Borders::BOTTOM,
Buffer::with_lines(vec![" ────────────┐ ", " ───────Title┘ "]),
);
// title bottom-right without right border
test_case(
Alignment::Right,
Borders::LEFT | Borders::TOP | Borders::BOTTOM,
Buffer::with_lines(vec![" ┌──────────── ", " └───────Title "]),
);
// title bottom-right without borders
test_case(
Alignment::Right,
Borders::NONE,
Buffer::with_lines(vec![" ", " Title "]),
);
}

114
tests/widgets_calendar.rs Normal file
View File

@@ -0,0 +1,114 @@
#![cfg(feature = "widget-calendar")]
use ratatui::{
backend::TestBackend,
buffer::Buffer,
style::Style,
widgets::{
calendar::{CalendarEventStore, Monthly},
Widget,
},
Terminal,
};
use time::{Date, Month};
fn test_render<W: Widget>(widget: W, expected: Buffer, size: (u16, u16)) {
let backend = TestBackend::new(size.0, size.1);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| f.render_widget(widget, f.size()))
.unwrap();
terminal.backend().assert_buffer(&expected);
}
#[test]
fn days_layout() {
let c = Monthly::new(
Date::from_calendar_date(2023, Month::January, 1).unwrap(),
CalendarEventStore::default(),
);
let expected = Buffer::with_lines(vec![
" 1 2 3 4 5 6 7",
" 8 9 10 11 12 13 14",
" 15 16 17 18 19 20 21",
" 22 23 24 25 26 27 28",
" 29 30 31",
]);
test_render(c, expected, (21, 5));
}
#[test]
fn days_layout_show_surrounding() {
let c = Monthly::new(
Date::from_calendar_date(2023, Month::December, 1).unwrap(),
CalendarEventStore::default(),
)
.show_surrounding(Style::default());
let expected = Buffer::with_lines(vec![
" 26 27 28 29 30 1 2",
" 3 4 5 6 7 8 9",
" 10 11 12 13 14 15 16",
" 17 18 19 20 21 22 23",
" 24 25 26 27 28 29 30",
" 31 1 2 3 4 5 6",
]);
test_render(c, expected, (21, 6));
}
#[test]
fn show_month_header() {
let c = Monthly::new(
Date::from_calendar_date(2023, Month::January, 1).unwrap(),
CalendarEventStore::default(),
)
.show_month_header(Style::default());
let expected = Buffer::with_lines(vec![
" January 2023 ",
" 1 2 3 4 5 6 7",
" 8 9 10 11 12 13 14",
" 15 16 17 18 19 20 21",
" 22 23 24 25 26 27 28",
" 29 30 31",
]);
test_render(c, expected, (21, 6));
}
#[test]
fn show_weekdays_header() {
let c = Monthly::new(
Date::from_calendar_date(2023, Month::January, 1).unwrap(),
CalendarEventStore::default(),
)
.show_weekdays_header(Style::default());
let expected = Buffer::with_lines(vec![
" Su Mo Tu We Th Fr Sa",
" 1 2 3 4 5 6 7",
" 8 9 10 11 12 13 14",
" 15 16 17 18 19 20 21",
" 22 23 24 25 26 27 28",
" 29 30 31",
]);
test_render(c, expected, (21, 6));
}
#[test]
fn show_combo() {
let c = Monthly::new(
Date::from_calendar_date(2023, Month::January, 1).unwrap(),
CalendarEventStore::default(),
)
.show_weekdays_header(Style::default())
.show_month_header(Style::default())
.show_surrounding(Style::default());
let expected = Buffer::with_lines(vec![
" January 2023 ",
" Su Mo Tu We Th Fr Sa",
" 1 2 3 4 5 6 7",
" 8 9 10 11 12 13 14",
" 15 16 17 18 19 20 21",
" 22 23 24 25 26 27 28",
" 29 30 31 1 2 3 4",
]);
test_render(c, expected, (21, 7));
}

42
tests/widgets_canvas.rs Normal file
View File

@@ -0,0 +1,42 @@
use ratatui::{
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,5 +1,7 @@
use tui::{
use ratatui::layout::Alignment;
use ratatui::{
backend::TestBackend,
buffer::Buffer,
layout::Rect,
style::{Color, Style},
symbols,
@@ -12,6 +14,247 @@ 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| {
let backend = TestBackend::new(width, height);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let datasets = vec![Dataset::default()
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Magenta))
.data(&[(0.0, 0.0)])];
let chart = Chart::new(datasets)
.block(Block::default().title("Plot").borders(Borders::ALL))
.x_axis(
Axis::default()
.bounds([0.0, 0.0])
.labels(create_labels(&["0.0", "1.0"])),
)
.y_axis(
Axis::default()
.bounds([0.0, 0.0])
.labels(create_labels(&["0.0", "1.0"])),
);
f.render_widget(chart, f.size());
})
.unwrap();
};
test_case(0, 0);
test_case(0, 1);
test_case(1, 0);
test_case(1, 1);
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);
@@ -124,3 +367,255 @@ fn widgets_chart_can_have_empty_datasets() {
})
.unwrap();
}
#[test]
fn widgets_chart_can_have_a_legend() {
let backend = TestBackend::new(60, 30);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let datasets = vec![
Dataset::default()
.name("Dataset 1")
.style(Style::default().fg(Color::Blue))
.data(&[
(0.0, 0.0),
(10.0, 1.0),
(20.0, 2.0),
(30.0, 3.0),
(40.0, 4.0),
(50.0, 5.0),
(60.0, 6.0),
(70.0, 7.0),
(80.0, 8.0),
(90.0, 9.0),
(100.0, 10.0),
])
.graph_type(Line),
Dataset::default()
.name("Dataset 2")
.style(Style::default().fg(Color::Green))
.data(&[
(0.0, 10.0),
(10.0, 9.0),
(20.0, 8.0),
(30.0, 7.0),
(40.0, 6.0),
(50.0, 5.0),
(60.0, 4.0),
(70.0, 3.0),
(80.0, 2.0),
(90.0, 1.0),
(100.0, 0.0),
])
.graph_type(Line),
];
let chart = Chart::new(datasets)
.style(Style::default().bg(Color::White))
.block(Block::default().title("Chart Test").borders(Borders::ALL))
.x_axis(
Axis::default()
.bounds([0.0, 100.0])
.title(Span::styled("X Axis", Style::default().fg(Color::Yellow)))
.labels(create_labels(&["0.0", "50.0", "100.0"])),
)
.y_axis(
Axis::default()
.bounds([0.0, 10.0])
.title("Y Axis")
.labels(create_labels(&["0.0", "5.0", "10.0"])),
);
f.render_widget(
chart,
Rect {
x: 0,
y: 0,
width: 60,
height: 30,
},
);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![
"┌Chart Test────────────────────────────────────────────────┐",
"│10.0│Y Axis ┌─────────┐│",
"│ │ •• │Dataset 1││",
"│ │ •• │Dataset 2││",
"│ │ •• └─────────┘│",
"│ │ •• •• │",
"│ │ •• •• │",
"│ │ •• •• │",
"│ │ •• •• │",
"│ │ •• •• │",
"│ │ •• •• │",
"│ │ •• •• │",
"│ │ ••• •• │",
"│ │ ••• │",
"│5.0 │ •• •• │",
"│ │ •• •• │",
"│ │ ••• •• │",
"│ │ •• •• │",
"│ │ •• •• │",
"│ │ •• •• │",
"│ │ •• •• │",
"│ │ •• •• │",
"│ │ •• •• │",
"│ │ •• ••• │",
"│ │ •• •• │",
"│ │ •• •• │",
"│0.0 │• X Axis│",
"│ └─────────────────────────────────────────────────────│",
"│ 0.0 50.0 100.0│",
"└──────────────────────────────────────────────────────────┘",
]);
// Set expected background color
for row in 0..30 {
for col in 0..60 {
expected.get_mut(col, row).set_bg(Color::White);
}
}
// Set expected colors of the first dataset
let line1 = vec![
(48, 5),
(49, 5),
(46, 6),
(47, 6),
(44, 7),
(45, 7),
(42, 8),
(43, 8),
(40, 9),
(41, 9),
(38, 10),
(39, 10),
(36, 11),
(37, 11),
(34, 12),
(35, 12),
(33, 13),
(30, 14),
(31, 14),
(28, 15),
(29, 15),
(25, 16),
(26, 16),
(27, 16),
(23, 17),
(24, 17),
(21, 18),
(22, 18),
(19, 19),
(20, 19),
(17, 20),
(18, 20),
(15, 21),
(16, 21),
(13, 22),
(14, 22),
(11, 23),
(12, 23),
(9, 24),
(10, 24),
(7, 25),
(8, 25),
(6, 26),
];
let legend1 = vec![
(49, 2),
(50, 2),
(51, 2),
(52, 2),
(53, 2),
(54, 2),
(55, 2),
(56, 2),
(57, 2),
];
for (col, row) in line1 {
expected.get_mut(col, row).set_fg(Color::Blue);
}
for (col, row) in legend1 {
expected.get_mut(col, row).set_fg(Color::Blue);
}
// Set expected colors of the second dataset
let line2 = vec![
(8, 2),
(9, 2),
(10, 3),
(11, 3),
(12, 4),
(13, 4),
(14, 5),
(15, 5),
(16, 6),
(17, 6),
(18, 7),
(19, 7),
(20, 8),
(21, 8),
(22, 9),
(23, 9),
(24, 10),
(25, 10),
(26, 11),
(27, 11),
(28, 12),
(29, 12),
(30, 12),
(31, 13),
(32, 13),
(33, 14),
(34, 14),
(35, 15),
(36, 15),
(37, 16),
(38, 16),
(39, 17),
(40, 17),
(41, 18),
(42, 18),
(43, 19),
(44, 19),
(45, 20),
(46, 20),
(47, 21),
(48, 21),
(49, 22),
(50, 22),
(51, 23),
(52, 23),
(53, 23),
(54, 24),
(55, 24),
(56, 25),
(57, 25),
];
let legend2 = vec![
(49, 3),
(50, 3),
(51, 3),
(52, 3),
(53, 3),
(54, 3),
(55, 3),
(56, 3),
(57, 3),
];
for (col, row) in line2 {
expected.get_mut(col, row).set_fg(Color::Green);
}
for (col, row) in legend2 {
expected.get_mut(col, row).set_fg(Color::Green);
}
// Set expected colors of the x axis
let x_axis_title = vec![(53, 26), (54, 26), (55, 26), (56, 26), (57, 26), (58, 26)];
for (col, row) in x_axis_title {
expected.get_mut(col, row).set_fg(Color::Yellow);
}
terminal.backend().assert_buffer(&expected);
}

View File

@@ -1,8 +1,11 @@
use tui::{
use ratatui::{
backend::TestBackend,
buffer::Buffer,
layout::{Constraint, Direction, Layout},
widgets::{Block, Borders, Gauge},
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
text::Span,
widgets::{Block, Borders, Gauge, LineGauge},
Terminal,
};
@@ -20,11 +23,82 @@ fn widgets_gauge_renders() {
let gauge = Gauge::default()
.block(Block::default().title("Percentage").borders(Borders::ALL))
.gauge_style(Style::default().bg(Color::Blue).fg(Color::Red))
.use_unicode(true)
.percent(43);
f.render_widget(gauge, chunks[0]);
let gauge = Gauge::default()
.block(Block::default().title("Ratio").borders(Borders::ALL))
.ratio(0.211_313_934_313_1);
.gauge_style(Style::default().bg(Color::Blue).fg(Color::Red))
.use_unicode(true)
.ratio(0.511_313_934_313_1);
f.render_widget(gauge, chunks[1]);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![
" ",
" ",
" ┌Percentage────────────────────────┐ ",
" │ ▋43% │ ",
" └──────────────────────────────────┘ ",
" ┌Ratio─────────────────────────────┐ ",
" │ 51% │ ",
" └──────────────────────────────────┘ ",
" ",
" ",
]);
for i in 3..17 {
expected
.get_mut(i, 3)
.set_bg(Color::Red)
.set_fg(Color::Blue);
}
for i in 17..37 {
expected
.get_mut(i, 3)
.set_bg(Color::Blue)
.set_fg(Color::Red);
}
for i in 3..20 {
expected
.get_mut(i, 6)
.set_bg(Color::Red)
.set_fg(Color::Blue);
}
for i in 20..37 {
expected
.get_mut(i, 6)
.set_bg(Color::Blue)
.set_fg(Color::Red);
}
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_gauge_renders_no_unicode() {
let backend = TestBackend::new(40, 10);
let mut terminal = Terminal::new(backend).unwrap();
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 gauge = Gauge::default()
.block(Block::default().title("Percentage").borders(Borders::ALL))
.percent(43)
.use_unicode(false);
f.render_widget(gauge, chunks[0]);
let gauge = Gauge::default()
.block(Block::default().title("Ratio").borders(Borders::ALL))
.ratio(0.211_313_934_313_1)
.use_unicode(false);
f.render_widget(gauge, chunks[1]);
})
.unwrap();
@@ -42,3 +116,138 @@ fn widgets_gauge_renders() {
]);
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);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let gauge = LineGauge::default()
.gauge_style(Style::default().fg(Color::Green).bg(Color::White))
.ratio(0.43);
f.render_widget(
gauge,
Rect {
x: 0,
y: 0,
width: 20,
height: 1,
},
);
let gauge = LineGauge::default()
.block(Block::default().title("Gauge 2").borders(Borders::ALL))
.gauge_style(Style::default().fg(Color::Green))
.line_set(symbols::line::THICK)
.ratio(0.211_313_934_313_1);
f.render_widget(
gauge,
Rect {
x: 0,
y: 1,
width: 20,
height: 3,
},
);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![
"43% ────────────────",
"┌Gauge 2───────────┐",
"│21% ━━━━━━━━━━━━━━│",
"└──────────────────┘",
]);
for col in 4..10 {
expected.get_mut(col, 0).set_fg(Color::Green);
}
for col in 10..20 {
expected.get_mut(col, 0).set_fg(Color::White);
}
for col in 5..7 {
expected.get_mut(col, 2).set_fg(Color::Green);
}
terminal.backend().assert_buffer(&expected);
}

View File

@@ -1,9 +1,10 @@
use tui::{
use ratatui::{
backend::TestBackend,
buffer::Buffer,
layout::Rect,
style::{Color, Style},
symbols,
text::Spans,
widgets::{Block, Borders, List, ListItem, ListState},
Terminal,
};
@@ -86,3 +87,141 @@ 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);
}
#[test]
fn widget_list_should_not_ignore_empty_string_items() {
let backend = TestBackend::new(6, 4);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let items = vec![
ListItem::new("Item 1"),
ListItem::new(""),
ListItem::new(""),
ListItem::new("Item 4"),
];
let list = List::new(items)
.style(Style::default())
.highlight_style(Style::default());
f.render_widget(list, f.size());
})
.unwrap();
let expected = Buffer::with_lines(vec!["Item 1", "", "", "Item 4"]);
terminal.backend().assert_buffer(&expected);
}

View File

@@ -1,9 +1,9 @@
use tui::{
use ratatui::{
backend::TestBackend,
buffer::Buffer,
layout::Alignment,
text::{Spans, Text},
widgets::{Block, Borders, Paragraph, Wrap},
text::{Span, Spans, Text},
widgets::{Block, Borders, Padding, Paragraph, Wrap},
Terminal,
};
@@ -15,7 +15,7 @@ const SAMPLE_STRING: &str = "The library is based on the principle of immediate
#[test]
fn widgets_paragraph_can_wrap_its_content() {
let test_case = |alignment, expected| {
let backend = TestBackend::new(20, 10);
let backend = TestBackend::new(22, 12);
let mut terminal = Terminal::new(backend).unwrap();
terminal
@@ -23,7 +23,12 @@ fn widgets_paragraph_can_wrap_its_content() {
let size = f.size();
let text = vec![Spans::from(SAMPLE_STRING)];
let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL))
.block(Block::default().borders(Borders::ALL).padding(Padding {
left: 2,
right: 2,
top: 1,
bottom: 1,
}))
.alignment(alignment)
.wrap(Wrap { trim: true });
f.render_widget(paragraph, size);
@@ -35,46 +40,52 @@ fn widgets_paragraph_can_wrap_its_content() {
test_case(
Alignment::Left,
Buffer::with_lines(vec![
"┌──────────────────┐",
"The library is",
"based on the ",
"principle of",
"immediate ",
"rendering with",
"intermediate ",
"buffers. This",
"means that at each",
"└──────────────────┘",
"┌────────────────────",
" ",
" The library is",
" based on the",
" principle of",
" immediate ",
" rendering with",
" intermediate ",
" buffers. This ",
"│ means that at │",
"│ │",
"└────────────────────┘",
]),
);
test_case(
Alignment::Right,
Buffer::with_lines(vec![
"┌──────────────────┐",
"The library is",
" based on the",
"principle of",
" immediate",
"rendering with",
" intermediate",
"buffers. This",
"means that at each",
"└──────────────────┘",
"┌────────────────────",
" ",
"The library is ",
"based on the ",
"principle of ",
" immediate ",
"rendering with ",
" intermediate ",
" buffers. This ",
"│ means that at │",
"│ │",
"└────────────────────┘",
]),
);
test_case(
Alignment::Center,
Buffer::with_lines(vec![
"┌──────────────────┐",
"The library is",
"based on the",
"principle of",
" immediate",
"rendering with",
"intermediate",
"buffers. This",
"means that at each",
"└──────────────────┘",
"┌────────────────────",
" ",
"The library is",
" based on the ",
"principle of",
" immediate ",
"rendering with",
" intermediate ",
" buffers. This ",
"│ means that at │",
"│ │",
"└────────────────────┘",
]),
);
}
@@ -141,6 +152,27 @@ fn widgets_paragraph_renders_mixed_width_graphemes() {
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_paragraph_can_wrap_with_a_trailing_nbsp() {
let nbsp: &str = "\u{00a0}";
let line = Spans::from(vec![Span::raw("NBSP"), Span::raw(nbsp)]);
let backend = TestBackend::new(20, 3);
let mut terminal = Terminal::new(backend).unwrap();
let expected = Buffer::with_lines(vec![
"┌──────────────────┐",
"│NBSP\u{00a0}",
"└──────────────────┘",
]);
terminal
.draw(|f| {
let size = f.size();
let paragraph = Paragraph::new(line).block(Block::default().borders(Borders::ALL));
f.render_widget(paragraph, size);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_paragraph_can_scroll_horizontally() {
let test_case = |alignment, scroll, expected| {

View File

@@ -1,8 +1,12 @@
use tui::backend::TestBackend;
use tui::buffer::Buffer;
use tui::layout::Constraint;
use tui::widgets::{Block, Borders, Row, Table};
use tui::Terminal;
use ratatui::{
backend::TestBackend,
buffer::Buffer,
layout::Constraint,
style::{Color, Modifier, Style},
text::{Span, Spans},
widgets::{Block, Borders, Cell, Row, Table, TableState},
Terminal,
};
#[test]
fn widgets_table_column_spacing_can_be_changed() {
@@ -13,16 +17,13 @@ fn widgets_table_column_spacing_can_be_changed() {
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(
["Head1", "Head2", "Head3"].iter(),
vec![
Row::Data(["Row11", "Row12", "Row13"].iter()),
Row::Data(["Row21", "Row22", "Row23"].iter()),
Row::Data(["Row31", "Row32", "Row33"].iter()),
Row::Data(["Row41", "Row42", "Row43"].iter()),
]
.into_iter(),
)
let table = Table::new(vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
])
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.widths(&[
Constraint::Length(5),
@@ -114,16 +115,13 @@ fn widgets_table_columns_widths_can_use_fixed_length_constraints() {
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(
["Head1", "Head2", "Head3"].iter(),
vec![
Row::Data(["Row11", "Row12", "Row13"].iter()),
Row::Data(["Row21", "Row22", "Row23"].iter()),
Row::Data(["Row31", "Row32", "Row33"].iter()),
Row::Data(["Row41", "Row42", "Row43"].iter()),
]
.into_iter(),
)
let table = Table::new(vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
])
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.widths(widths);
f.render_widget(table, size);
@@ -205,16 +203,13 @@ fn widgets_table_columns_widths_can_use_percentage_constraints() {
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(
["Head1", "Head2", "Head3"].iter(),
vec![
Row::Data(["Row11", "Row12", "Row13"].iter()),
Row::Data(["Row21", "Row22", "Row23"].iter()),
Row::Data(["Row31", "Row32", "Row33"].iter()),
Row::Data(["Row41", "Row42", "Row43"].iter()),
]
.into_iter(),
)
let table = Table::new(vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
])
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.widths(widths)
.column_spacing(0);
@@ -248,9 +243,9 @@ fn widgets_table_columns_widths_can_use_percentage_constraints() {
// columns of not enough width trims the data
test_case(
&[
Constraint::Percentage(10),
Constraint::Percentage(10),
Constraint::Percentage(10),
Constraint::Percentage(11),
Constraint::Percentage(11),
Constraint::Percentage(11),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
@@ -269,9 +264,9 @@ fn widgets_table_columns_widths_can_use_percentage_constraints() {
// columns of large width just before pushing a column off
test_case(
&[
Constraint::Percentage(30),
Constraint::Percentage(30),
Constraint::Percentage(30),
Constraint::Percentage(33),
Constraint::Percentage(33),
Constraint::Percentage(33),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
@@ -292,12 +287,12 @@ fn widgets_table_columns_widths_can_use_percentage_constraints() {
&[Constraint::Percentage(50), Constraint::Percentage(50)],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 │",
"│Head1 Head2 ",
"│ │",
"│Row11 Row12 │",
"│Row21 Row22 │",
"│Row31 Row32 │",
"│Row41 Row42 │",
"│Row11 Row12 ",
"│Row21 Row22 ",
"│Row31 Row32 ",
"│Row41 Row42 ",
"│ │",
"│ │",
"└────────────────────────────┘",
@@ -314,16 +309,13 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(
["Head1", "Head2", "Head3"].iter(),
vec![
Row::Data(["Row11", "Row12", "Row13"].iter()),
Row::Data(["Row21", "Row22", "Row23"].iter()),
Row::Data(["Row31", "Row32", "Row33"].iter()),
Row::Data(["Row41", "Row42", "Row43"].iter()),
]
.into_iter(),
)
let table = Table::new(vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
])
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.widths(widths);
f.render_widget(table, size);
@@ -356,18 +348,18 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
// columns of not enough width trims the data
test_case(
&[
Constraint::Percentage(10),
Constraint::Percentage(11),
Constraint::Length(20),
Constraint::Percentage(10),
Constraint::Percentage(11),
],
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 ",
"│ │",
"│ │",
"└────────────────────────────┘",
@@ -377,9 +369,9 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
// columns of large width just before pushing a column off
test_case(
&[
Constraint::Percentage(30),
Constraint::Percentage(33),
Constraint::Length(10),
Constraint::Percentage(30),
Constraint::Percentage(33),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
@@ -416,3 +408,416 @@ fn widgets_table_columns_widths_can_use_mixed_constraints() {
]),
);
}
#[test]
fn widgets_table_columns_widths_can_use_ratio_constraints() {
let test_case = |widths, expected| {
let backend = TestBackend::new(30, 10);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]),
])
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.widths(widths)
.column_spacing(0);
f.render_widget(table, size);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
// columns of zero width show nothing
test_case(
&[
Constraint::Ratio(0, 1),
Constraint::Ratio(0, 1),
Constraint::Ratio(0, 1),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
// columns of not enough width trims the data
test_case(
&[
Constraint::Ratio(1, 9),
Constraint::Ratio(1, 9),
Constraint::Ratio(1, 9),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│HeaHeaHea │",
"│ │",
"│RowRowRow │",
"│RowRowRow │",
"│RowRowRow │",
"│RowRowRow │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
// columns of large width just before pushing a column off
test_case(
&[
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row11 Row12 Row13 │",
"│Row21 Row22 Row23 │",
"│Row31 Row32 Row33 │",
"│Row41 Row42 Row43 │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
// percentages summing to 100 should give equal widths
test_case(
&[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 │",
"│ │",
"│Row11 Row12 │",
"│Row21 Row22 │",
"│Row31 Row32 │",
"│Row41 Row42 │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
}
#[test]
fn widgets_table_can_have_rows_with_multi_lines() {
let test_case = |state: &mut TableState, expected: Buffer| {
let backend = TestBackend::new(30, 8);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]).height(2),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]).height(2),
])
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.highlight_symbol(">> ")
.widths(&[
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
])
.column_spacing(1);
f.render_stateful_widget(table, size, state);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
let mut state = TableState::default();
// no selection
test_case(
&mut state,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row11 Row12 Row13 │",
"│Row21 Row22 Row23 │",
"│ │",
"│Row31 Row32 Row33 │",
"└────────────────────────────┘",
]),
);
// select first
state.select(Some(0));
test_case(
&mut state,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ Head1 Head2 Head3 │",
"│ │",
"│>> Row11 Row12 Row13 │",
"│ Row21 Row22 Row23 │",
"│ │",
"│ Row31 Row32 Row33 │",
"└────────────────────────────┘",
]),
);
// select second (we don't show partially the 4th row)
state.select(Some(1));
test_case(
&mut state,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ Head1 Head2 Head3 │",
"│ │",
"│ Row11 Row12 Row13 │",
"│>> Row21 Row22 Row23 │",
"│ │",
"│ Row31 Row32 Row33 │",
"└────────────────────────────┘",
]),
);
// select 4th (we don't show partially the 1st row)
state.select(Some(3));
test_case(
&mut state,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ Head1 Head2 Head3 │",
"│ │",
"│ Row31 Row32 Row33 │",
"│>> Row41 Row42 Row43 │",
"│ │",
"│ │",
"└────────────────────────────┘",
]),
);
}
#[test]
fn widgets_table_can_have_elements_styled_individually() {
let backend = TestBackend::new(30, 4);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = TableState::default();
state.select(Some(0));
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![
Row::new(vec!["Row11", "Row12", "Row13"]).style(Style::default().fg(Color::Green)),
Row::new(vec![
Cell::from("Row21"),
Cell::from("Row22").style(Style::default().fg(Color::Yellow)),
Cell::from(Spans::from(vec![
Span::raw("Row"),
Span::styled("23", Style::default().fg(Color::Blue)),
]))
.style(Style::default().fg(Color::Red)),
])
.style(Style::default().fg(Color::LightGreen)),
])
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
.highlight_symbol(">> ")
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
.widths(&[
Constraint::Length(6),
Constraint::Length(6),
Constraint::Length(6),
])
.column_spacing(1);
f.render_stateful_widget(table, size, &mut state);
})
.unwrap();
let mut expected = Buffer::with_lines(vec![
"│ Head1 Head2 Head3 │",
"│ │",
"│>> Row11 Row12 Row13 │",
"│ Row21 Row22 Row23 │",
]);
// First row = row color + highlight style
for col in 1..=28 {
expected.get_mut(col, 2).set_style(
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
);
}
// Second row:
// 1. row color
for col in 1..=28 {
expected
.get_mut(col, 3)
.set_style(Style::default().fg(Color::LightGreen));
}
// 2. cell color
for col in 11..=16 {
expected
.get_mut(col, 3)
.set_style(Style::default().fg(Color::Yellow));
}
for col in 18..=23 {
expected
.get_mut(col, 3)
.set_style(Style::default().fg(Color::Red));
}
// 3. text color
for col in 21..=22 {
expected
.get_mut(col, 3)
.set_style(Style::default().fg(Color::Blue));
}
terminal.backend().assert_buffer(&expected);
}
#[test]
fn widgets_table_should_render_even_if_empty() {
let backend = TestBackend::new(30, 4);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![])
.header(Row::new(vec!["Head1", "Head2", "Head3"]))
.block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
.widths(&[
Constraint::Length(6),
Constraint::Length(6),
Constraint::Length(6),
])
.column_spacing(1);
f.render_widget(table, size);
})
.unwrap();
let expected = Buffer::with_lines(vec![
"│Head1 Head2 Head3 │",
"│ │",
"│ │",
"│ │",
]);
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);
}

View File

@@ -1,4 +1,4 @@
use tui::{
use ratatui::{
backend::TestBackend, buffer::Buffer, layout::Rect, symbols, text::Spans, widgets::Tabs,
Terminal,
};