Compare commits

..

170 Commits

Author SHA1 Message Date
Josh McKinney
84243c7ce8 feat: add layout helper methods 2024-01-03 20:30:30 -08:00
Josh McKinney
6a50f2085e feat: add layout helper methods 2024-01-03 20:27:36 -08:00
Josh McKinney
beaa2bf58d feat(layout): add split_array method
Layout::split_array() makes it easy to store areas in individual values.
E.g.:

```rust
let [top, main] =
    Layout::vertical([Constraint::Length(1), Constraint::Min(0)])
    .split_array(area);
```
2024-01-03 20:27:36 -08:00
Orhun Parmaksız
50374b2456 docs(backend): fix broken book link (#733) 2024-01-03 07:23:59 -05:00
Akiomi Kamakura
49df5d4626 docs(example): fix markdown syntax for note (#730) 2024-01-02 17:20:50 -08:00
Josh McKinney
7ab12ed8ce feat(layout): add horizontal and vertical constructors (#728)
* feat(layout): add vertical and horizontal constructors

This commit adds two new constructors to the `Layout` struct, which
allow the user to create a vertical or horizontal layout with default
values.

```rust
let layout = Layout::vertical([
    Constraint::Length(10),
    Constraint::Min(5),
    Constraint::Length(10),
]);

let layout = Layout::horizontal([
    Constraint::Length(10),
    Constraint::Min(5),
    Constraint::Length(10),
]);
```
2024-01-02 15:59:33 -08:00
Valentin271
b459228e26 feat(termwiz): add From termwiz style impls (#726)
Important note: this also fixes a wrong mapping between ratatui's gray
and termwiz's grey. `ratatui::Color::Gray` now maps to
`termwiz::color::AnsiColor::Silver`
2024-01-02 13:19:14 -08:00
Josh McKinney
8f56fabcdd feat: accept Color and Modifier for all Styles (#720)
* feat: accept Color and Modifier for all Styles

All style related methods now accept `S: Into<Style>` instead of
`Style`.
`Color` and `Modifier` implement `Into<Style>` so this is allows for
more ergonomic usage. E.g.:

```rust
Line::styled("hello", Style::new().red());
Line::styled("world", Style::new().bold());

// can now be simplified to

Line::styled("hello", Color::Red);
Line::styled("world", Modifier::BOLD);
```

Fixes https://github.com/ratatui-org/ratatui/issues/694

BREAKING CHANGE: All style related methods now accept `S: Into<Style>`
instead of `Style`. This means that if you are already passing an
ambiguous type that implements `Into<Style>` you will need to remove
the `.into()` call.

`Block` style methods can no longer be called from a const context as
trait functions cannot (yet) be const.

* feat: add tuple conversions to Style

Adds conversions for various Color and Modifier combinations

* chore: add unit tests
2023-12-31 10:01:06 -08:00
Josh McKinney
a62632a947 refactor(buffer): split buffer module into files (#721) 2023-12-29 10:00:50 -08:00
Antonio Yang
f025d2bfa2 feat(table): Add Table::footer and Row::top_margin methods (#722)
* feat(table): Add a Table::footer method

Signed-off-by: Antonio Yang <yanganto@gmail.com>

* feat(table): Add a Row::top_margin method

- add Row::top_margin
- update table example

Signed-off-by: Antonio Yang <yanganto@gmail.com>

---------

Signed-off-by: Antonio Yang <yanganto@gmail.com>
2023-12-29 07:44:41 -08:00
Josh McKinney
63645333d6 refactor(table): split table into multiple files (#718)
At close to 2000 lines of code, the table widget was getting a bit
unwieldy. This commit splits it into multiple files, one for each
struct, and one for the table itself.

Also refactors the table rendering code to be easier to maintain.
2023-12-27 20:43:01 -08:00
Josh McKinney
5d410c6895 feat(line): implement Widget for Line (#715)
This allows us to use Line as a child of other widgets, and to use
Line::render() to render it rather than calling buffer.set_line().

```rust
frame.render_widget(Line::raw("Hello, world!"), area);
// or
Line::raw("Hello, world!").render(frame, area);
```
2023-12-27 20:30:47 +01:00
Orhun Parmaksız
8d77b734bb chore(ci): use cargo-nextest for running tests (#717)
* chore(ci): use cargo-nextest for running tests

* refactor(make): run library tests before doc tests
2023-12-27 10:50:56 -08:00
Josh McKinney
9574198958 refactor(line): reorder methods for natural reading order (#713) 2023-12-27 05:10:21 -08:00
Josh McKinney
ee54493163 fix(buffer): don't panic in set_style (#714)
This fixes a panic in set_style when the area to be styled is
outside the buffer's bounds.
2023-12-27 10:19:30 +01:00
Josh McKinney
c977293f14 feat(line)!: add style field, setters and docs (#708)
- The `Line` struct now stores the style of the line rather than each
  `Span` storing it.
- Adds two new setters for style and spans
- Adds missing docs

BREAKING CHANGE: `Line::style` is now a field of `Line` instead of being
stored in each `Span`.
2023-12-27 10:10:41 +01:00
Josh McKinney
b0ed658970 fix(table): render missing widths as equal (#710)
Previously, if `.widths` was not called before rendering a `Table`, no
content would render in the area of the table. This commit changes that
behaviour to default to equal widths for each column.

Fixes #510.

Co-authored-by: joshcbrown <80245312+joshcbrown@users.noreply.github.com>
2023-12-26 15:06:44 +01:00
Josh McKinney
37c183636b feat(span): implement Widget on Span (#709)
This allows us to use Span as a child of other widgets, and to use
Span::render() to render it rather than calling buffer.set_span().

```rust
frame.render_widget(Span::raw("Hello, world!"), area);
// or
Span::raw("Hello, world!").render(frame, area);
// or even
"Hello, world!".green().render(frame, area);
```
2023-12-26 15:05:57 +01:00
a-kenji
e67d3c64e0 docs(table): fix typo (#707) 2023-12-25 15:19:29 +01:00
Orhun Parmaksız
4f2db82a77 feat(color): use the FromStr implementation for deserialization (#705)
The deserialize implementation for Color used to support only the enum
names (e.g. Color, LightRed, etc.) With this change, you can use any of
the strings supported by the FromStr implementation (e.g. black,
light-red, #00ff00, etc.)
2023-12-23 19:38:53 +01:00
Valentin271
d6b851301e docs(examples): refactor chart example to showcase scatter (#703) 2023-12-22 16:57:26 -08:00
Josh McKinney
b7a479392e chore(ci): bump alpha release for breaking changes (#495)
Automatically detect breaking changes based on commit messages
and bump the alpha release number accordingly.

E.g. v0.23.1-alpha.1 will be bumped to v0.24.0-alpha.0 if any commit
since v0.23.0 has a breaking change.
2023-12-21 13:30:25 +01:00
a-kenji
e1cc849554 docs(breaking): fix typo (#702) 2023-12-18 16:06:49 +01:00
Orhun Parmaksız
7f5884829c chore(release): prepare for 0.25.0 (#699) 2023-12-18 13:06:28 +01:00
Orhun Parmaksız
a15c3b2660 docs: remove deprecated table constructor from breaking changes (#698) 2023-12-17 15:29:19 +01:00
Josh McKinney
41c44a4af6 docs(frame): add docs about resize events (#697) 2023-12-17 01:36:25 -08:00
Josh McKinney
1b8b6261e2 docs(examples): add animation and FPS counter to colors_rgb (#583) 2023-12-17 01:34:59 -08:00
Valentin271
5bf4f52119 feat: implement From trait for termion to Style related structs (#692)
* feat(termion): implement from termion color

* feat(termion): implement from termion style

* feat(termion): implement from termion `Bg` and `Fg`
2023-12-16 20:48:44 +01:00
Valentin271
f4c8de041d docs(chart): document chart module (#696) 2023-12-16 11:41:12 -08:00
Valentin271
910ad00059 chore(rustfmt): enable format_code_in_doc_comments (#695)
This enables more consistently formatted code in doc comments,
especially since ratatui heavily uses fluent setters.

See https://rust-lang.github.io/rustfmt/?version=v1.6.0#format_code_in_doc_comments
2023-12-16 13:01:07 +01:00
Valentin271
b282a06932 refactor!: remove items deprecated since 0.10 (#691)
Remove `Axis::title_style` and `Buffer::set_background` which are deprecated since 0.10
2023-12-15 16:22:34 +01:00
Lee Wonjoon
b8f71c0d6e feat(widgets/chart): add option to set the position of legend (#378) 2023-12-15 04:31:58 -08:00
Dheepak Krishnamurthy
113b4b7a4e docs: Rename template links to remove ratatui from name 📚 (#690) 2023-12-15 07:44:44 +01:00
Valentin271
b82451fb33 refactor(examples): add vim binding (#688) 2023-12-14 19:11:48 -08:00
Valentin271
4be18aba8b refactor(readme): reference awesome-ratatui instead of wiki (#689)
* refactor(readme): link awesome-ratatui instead of wiki

The apps wiki moved to awesome-ratatui

* Update README.md

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>

* Update README.md

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>

* Update README.md

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2023-12-14 20:45:36 +01:00
Valentin271
ebf1f42942 feat(style): implement From trait for crossterm to Style related structs (#686) 2023-12-14 13:25:36 +01:00
Josh McKinney
2169a0da01 docs(examples): Add example of half block rendering (#687)
This is a fun example of how to render big text using half blocks
2023-12-13 18:25:21 -08:00
Josh McKinney
d118565ef6 chore(table): cleanup docs and builder methods (#638)
- Refactor the `table` module for better top to bottom readability by
putting types first and arranging them in a logical order (Table, Row,
Cell, other).

- Adds new methods for:
  - `Table::rows`
  - `Row::cells`
  - `Cell::new`
  - `Cell::content`
  - `TableState::new`
  - `TableState::selected_mut`

- Makes `HighlightSpacing::should_add` pub(crate) since it's an internal
  detail.

- Adds tests for all the new methods and simple property tests for all
  the other setter methods.
2023-12-13 15:53:01 +01:00
YeungKC
aaeba2709c fix: truncate table when overflow (#685)
This prevents a panic when rendering an empty right aligned and rightmost table cell
2023-12-12 18:29:07 +01:00
Josh McKinney
d19b266e0e feat: add Constraint helpers (e.g. from_lengths) (#641)
Adds helper methods that convert from iterators of u16 values to the
specific Constraint type. This makes it easy to create constraints like:

```rust
// a fixed layout
let constraints = Constraint::from_lengths([10, 20, 10]);

// a centered layout
let constraints = Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]);
let constraints = Constraint::from_percentages([25, 50, 25]);

// a centered layout with a minimum size
let constraints = Constraint::from_mins([0, 100, 0]);

// a sidebar / main layout with maximum sizes
let constraints = Constraint::from_maxes([30, 200]);
```
2023-12-10 18:01:55 -08:00
Valentin271
f767ea7d37 refactor(List): start_corner is now direction (#673)
The previous name `start_corner` did not communicate clearly the intent of the method.
A new method `direction` and a new enum `ListDirection` were added.

`start_corner` is now deprecated
2023-12-10 16:50:13 -08:00
Josh McKinney
0576a8aa32 refactor(layout): to natural reading order (#681)
Structs and enums at the top of the file helps show the interaction
between the types without having to find each type in between longer
impl sections.

Also moved the try_split function into the Layout impl as an associated
function and inlined the `layout::split()` which just called try_split.
This makes the code a bit more contained.
2023-12-09 13:45:41 -08:00
Josh McKinney
03401cd46e ci: fix untrusted input in pr check workflow (#680) 2023-12-09 06:49:26 -08:00
Valentin271
f69d57c3b5 fix(Rect): fix underflow in the Rect::intersection method (#678) 2023-12-09 06:27:41 -08:00
Josh McKinney
2a87251152 docs(security): add security policy (#676)
* docs: Create SECURITY.md

* Update SECURITY.md

Co-authored-by: Orhun Parmaksız <orhun@archlinux.org>

---------

Co-authored-by: Orhun Parmaksız <orhun@archlinux.org>
2023-12-09 11:39:57 +01:00
Valentin271
aef495604c feat(List)!: List::new now accepts IntoIterator<Item = Into<ListItem>> (#672)
This allows to build list like

```
List::new(["Item 1", "Item 2"])
```

BREAKING CHANGE: `List::new` parameter type changed from `Into<Vec<ListItem<'a>>>`
to `IntoIterator<Item = Into<ListItem<'a>>>`
2023-12-08 14:20:49 -08:00
Tyler Bloom
8bfd6661e2 feat(Paragraph): add line_count and line_width unstable helper methods
This is an unstable feature that may be removed in the future
2023-12-07 18:14:56 +01:00
Valentin271
3ec4e24d00 docs(list): add documentation to the List widget (#669)
Adds documentation to the List widget and all its sub components like `ListState` and `ListItem`
2023-12-07 11:44:50 +01:00
Val Lorentz
7ced7c0aa3 refactor: define struct WrappedLine instead of anonymous tuple (#608)
It makes the type easier to document, and more obvious for users
2023-12-06 22:18:02 +01:00
tieway59
dd22e721e3 chore: Correct "builder methods" in docs and add must_use on widgets setters (#655)
Fixes #650

This PR corrects the "builder methods" expressing to simple `setters`
(see #650 #655), and gives a clearer diagnostic notice on setters `must_use`.

`#[must_use = "method moves the value of self and returns the modified value"]`

Details:

    docs: Correct wording in docs from builder methods

    Add `must_use` on layout setters

    chore: add `must_use` on widgets fluent methods

        This commit ignored `table.rs` because it is included in other PRs.

    test(gauge): fix test
2023-12-06 15:39:52 +01:00
Josh McKinney
4424637af2 feat(span): add setters for content and style (#647) 2023-12-05 01:17:53 -08:00
Josh McKinney
37c70dbb8e fix(table)!: Add widths parameter to new() (#664)
This prevents creating a table that doesn't actually render anything.

Fixes: https://github.com/ratatui-org/ratatui/issues/537

BREAKING CHANGE: Table::new() now takes an additional widths parameter.
2023-12-04 15:22:52 -08:00
Orhun Parmaksız
91c67eb100 docs(github): update code owners (#666)
onboard @Valentin271 as maintainer
2023-12-04 13:53:26 -08:00
Alan Somers
e49385b78c feat(table): Add a Table::segment_size method (#660)
It controls how to distribute extra space to an underconstrained table.
The default, legacy behavior is to leave the extra space unused.  The
new options are LastTakesRemainder which gets all space to the rightmost
column that can used it, and EvenDistribution which divides it amongst
all columns.

Fixes #370
2023-12-04 13:51:02 -08:00
Josh McKinney
6b2efd0f6c feat(layout): Accept IntoIterator for constraints (#663)
Layout and Table now accept IntoIterator for constraints with an Item
that is AsRef<Constraint>. This allows pretty much any collection of
constraints to be passed to the layout functions including arrays,
vectors, slices, and iterators (without having to call collect() on
them).
2023-12-04 10:46:21 -08:00
Josh McKinney
34d099c99a fix(tabs): fixup tests broken by semantic merge conflict (#665)
Two changes without any line overlap caused the tabs tests to break
2023-12-04 10:45:31 -08:00
Josh McKinney
987f7eed4c docs(website): rename book to website (#661) 2023-12-04 11:49:54 +01:00
Josh McKinney
e4579f0db2 fix(tabs)!: set the default highlight_style (#635)
Previously the default highlight_style was set to `Style::default()`,
which meant that the highlight style was the same as the normal style.
This change sets the default highlight_style to reversed text.

BREAKING CHANGE: The `Tab` widget now renders the highlight style as
reversed text by default. This can be changed by setting the
`highlight_style` field of the `Tab` widget.
2023-12-04 01:39:46 -08:00
Josh McKinney
6a6e9dde9d style(tabs): fix doc formatting (#662) 2023-12-04 01:38:57 -08:00
Rhaskia
28ac55bc62 fix(tabs): Tab widget now supports custom padding (#629)
The Tab widget now contains padding_left and and padding_right
properties. Those values can be set with functions `padding_left()`,
`padding_right()`, and `padding()` whic all accept `Into<Line>`.

Fixes issue https://github.com/ratatui-org/ratatui/issues/502
2023-12-03 16:38:43 -08:00
Orhun Parmaksız
458fa90362 docs(lib): tweak the crate documentation (#659) 2023-12-03 15:59:53 -08:00
Jan Ferdinand Sauer
56fc410105 fix(block): make inner aware of title positions (#657)
Previously, when computing the inner rendering area of a block, all
titles were assumed to be positioned at the top, which caused the
height of the inner area to be miscalculated.
2023-12-03 13:57:59 +01:00
Josh McKinney
753e246531 feat(layout): allow configuring layout fill (#633)
The layout split will generally fill the remaining area when `split()`
is called. This change allows the caller to configure how any extra
space is allocated to the `Rect`s. This is useful for cases where the
caller wants to have a fixed size for one of the `Rect`s, and have the
other `Rect`s fill the remaining space.

For now, the method and enum are marked as unstable because the exact
name is still being bikeshedded. To enable this functionality, add the
`unstable-segment-size` feature flag in your `Cargo.toml`.

To configure the layout to fill the remaining space evenly, use
`Layout::segment_size(SegmentSize::EvenDistribution)`. The default
behavior is `SegmentSize::LastTakesRemainder`, which gives the last
segment the remaining space. `SegmentSize::None` will disable this
behavior. See the docs for `Layout::segment_size()` and
`layout::SegmentSize` for more information.

Fixes https://github.com/ratatui-org/ratatui/issues/536
2023-12-02 12:21:13 -08:00
Josh McKinney
211160ca16 docs: remove simple-tui-rs (#651)
This has not been recently and doesn't lead to good code
2023-12-02 16:17:50 +01:00
Valentin271
1229b96e42 feat(Rect): add offset method (#533)
The offset method creates a new Rect that is moved by the amount
specified in the x and y direction. These values can be positive or
negative. This is useful for manual layout tasks.

```rust
let rect = area.offset(Offset { x: 10, y -10 });
```
2023-11-27 08:03:18 -08:00
Valentin271
fe632d70cb docs(Sparkline): add documentation (#648) 2023-11-26 15:46:20 -08:00
Orhun Parmaksız
c862aa5e9e feat(list): support line alignment (#599)
The `List` widget now respects the alignment of `Line`s and renders them as expected.
2023-11-23 11:52:12 -08:00
Josh McKinney
18e19f6ce6 chore: fix breaking changes doc versions (#639)
Moves the layout::new change to unreleasedd section and adds the table change
2023-11-22 23:59:06 +01:00
Linda_pp
7ef0afcb62 refactor(widgets): remove unnecessary dynamic dispatch and heap allocation (#597)
Signed-off-by: rhysd <lin90162@yahoo.co.jp>
2023-11-21 22:24:54 -08:00
Josh McKinney
1e2f0be75a feat(layout)!: add parameters to Layout::new() (#557)
Adds a convenience function to create a layout with a direction and a
list of constraints which are the most common parameters that would be
generally configured using the builder pattern. The constraints can be
passed in as any iterator of constraints.

```rust
let layout = Layout::new(Direction::Horizontal, [
    Constraint::Percentage(50),
    Constraint::Percentage(50),
]);
```

BREAKING CHANGE:
Layout::new() now takes a direction and a list of constraints instead of
no arguments. This is a breaking change because it changes the signature
of the function. Layout::new() is also no longer const because it takes
an iterator of constraints.
2023-11-21 14:34:08 -08:00
Leon Sautour
a58cce2dba chore: disable default benchmarking (#598)
Disables the default benchmarking behaviour for the lib target to fix unrecognized
criterion benchmark arguments.

See https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options for details
2023-11-21 14:23:21 -08:00
Jonathan Chan Kwan Yin
ffa78aa67c fix: add #[must_use] to Style-moving methods (#600) 2023-11-21 14:21:28 -08:00
dependabot[bot]
7cbb1060ac chore(deps): update itertools requirement from 0.11 to 0.12 (#636)
Updates the requirements on [itertools](https://github.com/rust-itertools/itertools) to permit the latest version.
- [Changelog](https://github.com/rust-itertools/itertools/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-itertools/itertools/compare/v0.11.0...v0.12.0)

---
updated-dependencies:
- dependency-name: itertools
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-20 14:47:59 -08:00
dependabot[bot]
a05541358e chore(deps): bump actions/github-script from 6 to 7 (#637)
Bumps [actions/github-script](https://github.com/actions/github-script) from 6 to 7.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-20 14:43:06 -08:00
Josh McKinney
1f88da7538 fix(table): fix new clippy lint which triggers on table widths tests (#630)
* fix(table): new clippy lint in 1.74.0 triggers on table widths tests

https://rust-lang.github.io/rust-clippy/master/index.html\#/needless_borrows_for_generic_args

* fix(clippy): fix beta lint for .get(0) -> .first()

https://rust-lang.github.io/rust-clippy/master/index.html\#/get_first
2023-11-16 18:52:32 +01:00
Josh McKinney
36d8c53645 fix(table): widths() now accepts AsRef<[Constraint]> (#628)
This allows passing an array, slice or Vec of constraints, which is more
ergonomic than requiring this to always be a slice.

The following calls now all succeed:

```rust
Table::new(rows).widths([Constraint::Length(5), Constraint::Length(5)]);
Table::new(rows).widths(&[Constraint::Length(5), Constraint::Length(5)]);

// widths could also be computed at runtime
let widths = vec![Constraint::Length(5), Constraint::Length(5)];
Table::new(rows).widths(widths.clone());
Table::new(rows).widths(&widths);
```
2023-11-15 12:34:02 -08:00
Linda_pp
ec7b3872b4 fix(doc): do not access deprecated Cell::symbol field in doc example (#626) 2023-11-13 06:29:01 -08:00
Linda_pp
edacaf7ff4 feat(buffer): deprecate Cell::symbol field (#624)
The Cell::symbol field is now accessible via a getter method (`symbol()`). This will
allow us to make future changes to the Cell internals such as replacing `String` with
`compact_str`.
2023-11-11 21:43:51 -08:00
Danny Burrows
df0eb1f8e9 fix(terminal): insert_before() now accepts lines > terminal height and doesn't add an extra blank line (#596)
Fixes issue with inserting content with height>viewport_area.height and adds
the ability to insert content of height>terminal_height

- Adds TestBackend::append_lines() and TestBackend::clear_region() methods to
  support testing the changes
2023-11-08 10:04:35 -08:00
Josh McKinney
59b9c32fbc ci(codecov): adjust threshold and noise settings (#615)
Fixes https://github.com/ratatui-org/ratatui/issues/612
2023-11-05 10:21:11 +01:00
isinstance
9f37100096 Update README.md and fix the bug that demo2 cannot run (#595)
Fixes https://github.com/ratatui-org/ratatui/issues/594
2023-10-25 14:01:04 -07:00
a-kenji
a2f2bd5df5 fix: MSRV is now 1.70.0 (#593) 2023-10-25 02:44:36 -07:00
Orhun Parmaksız
c597b87f72 chore(release): prepare for 0.24.0 (#588) 2023-10-23 04:06:53 -07:00
Orhun Parmaksız
82a0d01a42 chore(changelog): skip dependency updates in changelog (#582) 2023-10-23 04:04:26 -07:00
Orhun Parmaksız
0e573cd6c7 docs(github): update code owners (#587) 2023-10-23 04:03:51 -07:00
Josh McKinney
b07000835f docs(readme): fix link to demo2 image (#589) 2023-10-23 12:28:59 +02:00
Orhun Parmaksız
c6c3f88a79 feat(backend): implement common traits for WindowSize (#586) 2023-10-23 10:40:09 +02:00
dependabot[bot]
a20bd6adb5 chore(deps): update lru requirement from 0.11.1 to 0.12.0 (#581)
Updates the requirements on [lru](https://github.com/jeromefroe/lru-rs) to permit the latest version.
- [Changelog](https://github.com/jeromefroe/lru-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jeromefroe/lru-rs/compare/0.11.1...0.12.0)

---
updated-dependencies:
- dependency-name: lru
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-21 09:25:05 -04:00
dependabot[bot]
5213f78d25 chore(deps): bump actions/checkout from 3 to 4 (#580)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-21 12:19:52 +02:00
Josh McKinney
12f92911c7 chore(github): create dependabot.yml (#575)
* chore: Create dependabot.yml

* Update .github/dependabot.yml

Co-authored-by: Orhun Parmaksız <orhun@archlinux.org>

* Update .github/dependabot.yml

---------

Co-authored-by: Orhun Parmaksız <orhun@archlinux.org>
2023-10-21 12:17:47 +02:00
Josh McKinney
ad2dc5646d docs(examples): update examples readme (#576)
remove VHS bug info, tweak colors_rgb image, update some of the instructions. add demo2
2023-10-20 21:41:36 +02:00
Linda_pp
6cbdb06fd8 chore(examples): refactor some examples (#578)
* chore(examples): Simplify timeout calculation with `Duration::saturating_sub`

Signed-off-by: rhysd <lin90162@yahoo.co.jp>

* fix(examples): Do not ignore errors from `poll_input` call

Signed-off-by: rhysd <lin90162@yahoo.co.jp>

---------

Signed-off-by: rhysd <lin90162@yahoo.co.jp>
2023-10-20 21:31:52 +02:00
blu
27c5637675 docs(README): fix links to CONTRIBUTING.md and BREAKING-CHANGES.md (#577) 2023-10-20 20:29:04 +02:00
Josh McKinney
0c52ff431a fix(gauge): fix gauge widget colors (#572)
The background colors of the gauge had a workaround for the issue we had
with VHS / TTYD rendering the background color of the gauge. This
workaround is no longer necessary in the updated versions of VHS / TTYD.

Fixes https://github.com/ratatui-org/ratatui/issues/501
2023-10-19 04:29:53 -07:00
Josh McKinney
8d507c43fa fix(backend): add feature flag for underline-color (#570)
Windows 7 doesn't support the underline color attribute, so we need to
make it optional. This commit adds a feature flag for the underline
color attribute - it is enabled by default, but can be disabled by
passing `--no-default-features` to cargo.

We could specically check for Windows 7 and disable the feature flag
automatically, but I think it's better for this check to be done by the
crossterm crate, since it's the one that actually knows about the
underlying terminal.

To disable the feature flag in an application that supports Windows 7,
add the following to your Cargo.toml:

```toml
ratatui = { version = "0.24.0", default-features = false, features = ["crossterm"] }
```

Fixes https://github.com/ratatui-org/ratatui/issues/555
2023-10-18 13:52:43 -07:00
Josh McKinney
b61f65bc20 docs(examples): udpate theme to Aardvark Blue (#574)
This is a nicer theme that makes the colors pop
2023-10-18 13:35:51 +02:00
Orhun Parmaksız
3a57e76ed1 chore(github): add contact links for issues (#567) 2023-10-11 17:42:48 -07:00
tz629
6c7bef8d11 docs: replace colons with dashes in README.md for consistency (#566) 2023-10-07 23:37:09 -07:00
Dheepak Krishnamurthy
88ae3485c2 docs: Update Frame docstring to remove reference to generic backend (#564) 2023-10-06 17:31:36 -04:00
Dheepak Krishnamurthy
e5caf170c8 docs(custom_widget): make button sticky when clicking with mouse (#561) 2023-10-06 11:17:14 +02:00
Dheepak Krishnamurthy
089f8ba66a docs: Add double quotes to instructions for features (#560) 2023-10-05 04:06:28 -04:00
Josh McKinney
fbf1a451c8 chore: simplify constraints (#556)
Use bare arrays rather than array refs / Vecs for all constraint
examples.

Ref: https://github.com/ratatui-org/ratatui-book/issues/94
2023-10-03 16:50:14 -07:00
Josh McKinney
4541336514 feat(canvas): implement half block marker (#550)
* feat(canvas): implement half block marker

A useful technique for the terminal is to use half blocks to draw a grid
of "pixels" on the screen. Because we can set two colors per cell, and
because terminal cells are about twice as tall as they are wide, we can
draw a grid of half blocks that looks like a grid of square pixels.

This commit adds a new `HalfBlock` marker that can be used in the Canvas
widget and the associated HalfBlockGrid.

Also updated demo2 to use the new marker as it looks much nicer.

Adds docs for many of the methods and structs on canvas.

Changes the grid resolution method to return the pixel count
rather than the index of the last pixel.
This is an internal detail with no user impact.
2023-09-30 06:03:03 -07:00
Josh McKinney
346e7b4f4d docs: add summary to breaking changes (#549) 2023-09-30 05:54:38 -07:00
Dheepak Krishnamurthy
15641c8475 feat: Add buffer_mut method on Frame (#548) 2023-09-30 05:53:37 -07:00
Hichem
2fd85af33c refactor(barchart): simplify internal implementation (#544)
Replace `remove_invisible_groups_and_bars` with `group_ticks`
`group_ticks` calculates the visible bar length in ticks. (A cell contains 8 ticks).

It is used for 2 purposes:
1. to get the bar length in ticks for rendering
2. since it delivers only the values of the visible bars, If we zip these values
   with the groups and bars, then we will filter out the invisible groups and bars

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>
2023-09-29 15:04:20 -07:00
Dheepak Krishnamurthy
401a7a7f71 docs: Improve clarity in documentation for Frame and Terminal 📚 (#545) 2023-09-28 20:18:54 -07:00
Dheepak Krishnamurthy
e35e4135c9 docs: Fix terminal comment (#547) 2023-09-28 18:44:52 -07:00
Dheepak Krishnamurthy
8ae4403b63 docs: Fix Terminal docstring (#546) 2023-09-28 18:42:31 -07:00
Josh McKinney
11076d0af3 fix(rect): fix arithmetic overflow edge cases (#543)
Fixes https://github.com/ratatui-org/ratatui/issues/258
2023-09-28 03:19:33 -07:00
Josh McKinney
9cfb133a98 docs: document alpha release process (#542)
Fixes https://github.com/ratatui-org/ratatui/issues/412
2023-09-28 11:27:03 +02:00
Josh McKinney
4548a9b7e2 docs: add BREAKING-CHANGES.md (#538)
Document the breaking changes in each version. This document is
manually curated by summarizing the breaking changes in the changelog.
2023-09-28 01:00:43 -07:00
Josh McKinney
61af0d9906 docs(examples): make custom widget example into a button (#539)
The widget also now supports mouse
2023-09-27 20:07:45 -07:00
Josh McKinney
c0991cc576 docs: make library and README consistent (#526)
* docs: make library and README consistent

Generate the bulk of the README from the library documentation, so that
they are consistent using cargo-rdme.

- Removed the Contributors section, as it is redundant with the github
  contributors list.
- Removed the info about the other backends and replaced it with a
  pointer to the documentation.
- add docsrs example, vhs tape and images that will end up in the README

Fixes: https://github.com/ratatui-org/ratatui/issues/512
2023-09-27 19:57:04 -07:00
Hichem
301366c4fa feat(barchart): render charts smaller than 3 lines (#532)
The bar values are not shown if the value width is equal the bar width
and the bar is height is less than one line

Add an internal structure `LabelInfo` which stores the reserved height
for the labels (0, 1 or 2) and also whether the labels will be shown.

Fixes ratatui-org#513

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>
2023-09-26 13:54:04 -07:00
Valentin271
3bda372847 docs(tabs): add documentation to Tabs (#535) 2023-09-25 23:22:14 -07:00
Josh McKinney
082cbcbc50 feat(frame)!: Remove generic Backend parameter (#530)
This change simplifys UI code that uses the Frame type. E.g.:

```rust
fn draw<B: Backend>(frame: &mut Frame<B>) {
    // ...
}
```

Frame was generic over Backend because it stored a reference to the
terminal in the field. Instead it now directly stores the viewport area
and current buffer. These are provided at creation time and are valid
for the duration of the frame.

BREAKING CHANGE: Frame is no longer generic over Backend. Code that
accepted `Frame<Backend>` will now need to accept `Frame`. To migrate
existing code, remove any generic parameters from code that uses an
instance of a Frame. E.g. the above code becomes:

```rust
fn draw(frame: &mut Frame) {
    // ...
}
```
2023-09-25 22:30:36 -07:00
Josh McKinney
cbf86da0e7 feat(rect): add is_empty() to simplify some common checks (#534)
- add `Rect::is_empty()` that checks whether either height or width == 0
- refactored `Rect` into layout/rect.rs from layout.rs. No public API change as
   the module is private and the type is re-exported under the `layout` module.
2023-09-25 21:45:29 -07:00
Josh McKinney
32e461953c feat(block)!: allow custom symbols for borders (#529)
Adds a new `Block::border_set` method that allows the user to specify
the symbols used for the border.

Added two new border types: `BorderType::QuadrantOutside` and
`BorderType::QuadrantInside`. These are used to draw borders using the
unicode quadrant characters (which look like half block "pixels").

QuadrantOutside:
```
▛▀▀▜
▌  ▐
▙▄▄▟
```

QuadrantInside:
```
▗▄▄▖
▐  ▌
▝▀▀▘
```
Fixes: https://github.com/ratatui-org/ratatui/issues/528

BREAKING CHANGES:
- BorderType::to_line_set is renamed to to_border_set
- BorderType::line_symbols is renamed to border_symbols
2023-09-23 22:08:32 -07:00
Orhun Parmaksız
d67fa2c00d feat(line): add Line::raw constructor (#511)
* feat(line): add `Line::raw` constructor

There is already `Span::raw` and `Text::raw` methods
and this commit simply adds `Line::raw` method for symmetry.

Multi-line content is converted to multiple spans with the new line removed
2023-09-22 04:34:58 -07:00
Valentin271
c3155a2489 fix(barchart): add horizontal labels(#518)
Labels were missed in the initial implementation of the horizontal
mode for the BarChart widget. This adds them.

Fixes https://github.com/ratatui-org/ratatui/issues/499
2023-09-22 00:38:58 -07:00
Josh McKinney
c5ea656385 fix(barchart): avoid divide by zero in rendering (#525)
Fixes: https://github.com/ratatui-org/ratatui/issues/521
2023-09-21 16:35:28 -07:00
Josh McKinney
be55a5fbcd feat(examples): add demo2 example (#500) 2023-09-21 01:47:23 -07:00
BlakStar
21303f2167 fix(rect): prevent overflow in inner() and area() (#523) 2023-09-20 15:56:32 -07:00
Hichem
c9b8e7cf41 fix(barchart): render value labels with unicode correctly (#515)
An earlier change introduced a bug where the width of value labels with
unicode characters was incorrectly using the string length in bytes
instead of the unicode character count. This reverts the earlier change.

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>
2023-09-19 18:06:32 -07:00
Josh McKinney
5498a889ae chore(spans): remove deprecated Spans type (#426)
The `Spans` type (plural, not singular) was replaced with a more ergonomic `Line` type
in Ratatui v0.21.0 and marked deprecated byt left for backwards compatibility. This is now
removed.

- `Line` replaces `Spans`
- `Buffer::set_line` replaces `Buffer::set_spans`
2023-09-19 02:58:52 -07:00
Valentin271
0fe738500c docs(Gauge): add docs for Gauge and LineGauge (#514) 2023-09-18 15:21:59 -07:00
Aizon
dd9a8df03a docs(table): add documentation for block and header methods of the Table widget (#505) 2023-09-17 14:19:21 -07:00
Josh McKinney
0c7d547db1 fix(docs): don't fail rustdoc due to termion (#503)
Windows cannot compile termion, so it is not included in the docs.
Rustdoc will fail if it cannot find a link, so the docs fail to build
on windows.

This replaces the link to TermionBackend with one that does not fail
during checks.

Fixes https://github.com/ratatui-org/ratatui/issues/498
2023-09-15 09:47:10 -07:00
Mariano Marciello
638d596a3b fix(Layout): use LruCache for layout cache (#487)
The layout cache now uses a LruCache with default size set to 16 entries.
Previously the cache was backed by a HashMap, and was able to grow
without bounds as a new entry was added for every new combination of
layout parameters.

- Added a new method (`layout::init_cache(usize)`) that allows the cache
size to be changed if necessary. This will only have an effect if it is called
prior to any calls to `layout::split()` as the cache is wrapped in a `OnceLock`
2023-09-14 15:20:38 -07:00
Josh McKinney
d4976d4b63 docs(widgets): update the list of available widgets (#496) 2023-09-13 10:48:00 +02:00
Josh McKinney
a7bf4b3f36 chore: use modern modules syntax (#492)
Move xxx/mod.rs to xxx.rs
2023-09-12 12:38:51 -07:00
Josh McKinney
94af2a29e1 test(buffer): allow with_lines to accept Vec<Into<Line>> (#494)
This allows writing unit tests without having to call set_style on the
expected buffer.

E.g.:
```rust
use crate::style::Stylize;
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
buf.set_string(0, 0, "foo", Style::new().red());
buf.set_string(0, 1, "bar", Style::new().blue());
assert_eq!(buf, Buffer::with_lines(vec!["foo".red(), "bar".blue()]));
```

Inspired by https://github.com/ratatui-org/ratatui/issues/493#issuecomment-1714844468
2023-09-12 11:53:43 -07:00
Josh McKinney
42f816999e docs(terminal): add docs for terminal module (#486)
- moves the impl Terminal block up to be closer to the type definition
2023-09-11 18:39:15 -07:00
Josh McKinney
1414fbcc05 docs: import prelude::* in doc examples (#490)
This commit adds `prelude::*` all doc examples and widget::* to those
that need it. This is done to highlight the use of the prelude and
simplify the examples.

- Examples in Type and module level comments show all imports and use
  `prelude::*` and `widget::*` where possible.
- Function level comments hide imports unless there are imports other
  than `prelude::*` and `widget::*`.
2023-09-11 18:01:57 -07:00
Josh McKinney
1947c58c60 docs(backend): improve backend module docs (#489) 2023-09-11 17:36:44 -07:00
Josh McKinney
1e20475061 docs(stylize): improve docs for style shorthands (#491)
The Stylize trait was introduced in 0.22 to make styling less verbose.
This adds a bunch of documentation comments to the style module and
types to make this easier to discover.
2023-09-11 16:46:03 -07:00
Dheepak Krishnamurthy
af36282df5 chore: only run check pr action on pull_request_target events (#485) 2023-09-10 05:19:03 -04:00
Dheepak Krishnamurthy
322e46f15d chore: Prevent PR merge with do not merge labels ♻️ (#484) 2023-09-10 04:14:51 -04:00
Dheepak Krishnamurthy
983ea7f7a5 chore: Fix check for if breaking change label should be added ♻️ (#483) 2023-09-10 03:29:06 -04:00
Dheepak Krishnamurthy
384e616231 chore: Add a check for if breaking change label should be added ♻️ (#481) 2023-09-09 21:06:31 -04:00
Josh McKinney
6b8725f091 docs(examples): add colors_rgb example (#476) 2023-09-09 17:30:41 -07:00
Josh McKinney
17797d83da docs(canvas): add support note for Braille marker (#472) 2023-09-09 17:08:28 -07:00
Josh McKinney
ebd3680a47 fix(stylize)!: add Stylize impl for String (#466)
Although the `Stylize` trait is already implemented for `&str` which
extends to `String`, it is not implemented for `String` itself. This
commit adds an impl of Stylize that returns a Span<'static> for `String`
so that code can call Stylize methods on temporary `String`s.

E.g. the following now compiles instead of failing with a compile error
about referencing a temporary value:

    let s = format!("hello {name}!", "world").red();

BREAKING CHANGE: This may break some code that expects to call Stylize
methods on `String` values and then use the String value later. This
will now fail to compile because the String is consumed by set_style
instead of a slice being created and consumed.

This can be fixed by cloning the `String`. E.g.:

    let s = String::from("hello world");
    let line = Line::from(vec![s.red(), s.green()]); // fails to compile
    let line = Line::from(vec![s.clone().red(), s.green()]); // works
2023-09-09 17:05:36 -07:00
Josh McKinney
74c5244be1 docs: add logo and favicon to docs.rs page (#473) 2023-09-09 17:04:16 -07:00
Josh McKinney
3cf0b83bda docs(color): document true color support (#477)
* refactor(style): move Color to separate color mod

* docs(color): document true color support
2023-09-09 07:41:00 -07:00
Orhun Parmaksız
127813120e chore(changelog): make the scopes lowercase in the changelog (#479) 2023-09-09 07:29:41 -07:00
Valentin271
0c68ebed4f docs(Block): add documentation to Block (#469) 2023-09-06 15:09:41 -07:00
Aizon
232be80325 docs(table): add documentation for Table::new() (#471) 2023-09-05 16:41:47 -07:00
Josh McKinney
c95a75c5d5 ci(makefile): remove termion dependency from doc lint (#470)
Only build termion on non-windows targets
2023-09-05 16:39:34 -07:00
Aatu Kajasto
c8ab2d5908 fix(chart): use graph style for top line (#462)
A bug in the rendering caused the top line of the chart to be rendered
using the style of the chart, instead of the dataset style. This is
fixed by only setting the style for the width of the text, and not the
entire row.

Fixes: https://github.com/ratatui-org/ratatui/issues/379
2023-09-05 05:51:05 -07:00
Josh McKinney
572df758ba ci: put commit id first in changelog (#463) 2023-09-05 02:13:02 -07:00
Josh McKinney
b996102837 ci(makefile): add format target (#468)
- add format target to Makefile.toml that actually fixes the formatting
- rename fmt target to lint-format
- rename style-check target to lint-style
- rename typos target to lint-typos
- rename check-docs target to lint-docs
- add section to CONTRIBUTING.md about formatting
2023-09-05 00:48:36 -07:00
onotaizee@gmail.com
080a05bbd3 docs(paragraph): add docs for alignment fn (#467) 2023-09-04 22:42:32 -07:00
Josh McKinney
343c6cdc47 ci(lint): move formatting and doc checks first (#465)
Putting the formatting and doc checks first to ensure that more critical
errors are caught first (e.g. a conventional commit error or typo should
not prevent the formatting and doc checks from running).
2023-09-03 01:30:00 +02:00
Josh McKinney
5c785b2270 docs(examples): move example gifs to github (#460)
- A new orphan branch named "images" is created to store the example
  images
2023-09-02 14:15:07 -07:00
Josh McKinney
ca9bcd3156 docs(examples): add descriptions and update theme (#460)
- Use the OceanicMaterial consistently in examples
2023-09-02 14:15:03 -07:00
Orhun Parmaksız
82b40be4ab chore(ci): improve checking the PR title (#464)
- Use [`action-semantic-pull-request`](https://github.com/amannn/action-semantic-pull-request)
- Allow only reading the PR contents
- Enable merge group
2023-09-02 14:11:21 -07:00
Valentin271
e098731d6c docs(barchart): add documentation to BarChart (#449)
Add documentation to the `BarChart` widgets and its sub-modules.
2023-09-01 19:59:35 -07:00
Valentin271
5f6aa30be5 chore: check documentation lint (#454) 2023-09-01 19:59:07 -07:00
Valentin271
ea70bffe5d test(barchart): add benchmarks (#455) 2023-09-01 19:57:48 -07:00
Dheepak Krishnamurthy
47ae602df4 chore: Check that PR title matches conventional commit guidelines ♻️ (#459)
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2023-09-01 19:56:40 -07:00
Josh McKinney
878b6fc258 ci: ignore benches from code coverage (#461) 2023-09-02 02:45:46 +00:00
Dheepak Krishnamurthy
0696f484e8 Better ergonomics for ScrollbarState and improved documentation (#456)
* feat(scrollbar): Better ergonomics for ScrollbarState and improved documentation 

* feat(scrollbar)!: Use usize instead of u16 for scrollbar  💥
2023-09-01 06:47:14 +00:00
Valentin271
927a5d8251 docs: fix documentation lint warnings (#450) 2023-08-30 10:01:42 +00:00
a-kenji
28e7fd4bc5 docs(terminal): fix doc comment (#452) 2023-08-30 10:01:28 +00:00
Valentin271
28c61571e8 chore: add documentation guidelines (#447) 2023-08-29 20:57:46 +00:00
Benjamin Grosse
d0779034e7 feat(backend): backend provides window_size, add Size struct (#276)
For image (sixel, iTerm2, Kitty...) support that handles graphics in
terms of `Rect` so that the image area can be included in layouts.

For example: an image is loaded with a known pixel-size, and drawn, but
the image protocol has no mechanism of knowing the actual cell/character
area that been drawn on. It is then impossible to skip overdrawing the
area.

Returning the window size in pixel-width / pixel-height, together with
colums / rows, it can be possible to account the pixel size of each cell
/ character, and then known the `Rect` of a given image, and also resize
the image so that it fits exactly in a `Rect`.

Crossterm and termwiz also both return both sizes from one syscall,
while termion does two.

Add a `Size` struct for the cases where a `Rect`'s `x`/`y` is unused
(always zero).

`Size` is not "clipped" for `area < u16::max_value()` like `Rect`. This
is why there are `From` implementations between the two.
2023-08-29 04:46:02 +00:00
Valentin271
51fdcbe7e9 docs(title): add documentation to title (#443)
This adds documentation for Title and Position
2023-08-29 02:18:02 +00:00
Dheepak Krishnamurthy
eda2fb7077 docs: Use ratatui 📚 (#446) 2023-08-29 01:40:43 +00:00
151 changed files with 20051 additions and 6724 deletions

2
.github/CODEOWNERS vendored
View File

@@ -5,4 +5,4 @@
# https://git-scm.com/docs/gitignore#_pattern_format
# Maintainers
* @orhun @mindoodoo @sayanarijit @sophacles @joshka @kdheepak
* @orhun @mindoodoo @sayanarijit @joshka @kdheepak @Valentin271

View File

@@ -1 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Discord Chat
url: https://discord.gg/pMCEU9hNEj
about: Ask questions about ratatui on Discord
- name: Matrix Chat
url: https://matrix.to/#/#ratatui:matrix.org
about: Ask questions about ratatui on Matrix

18
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
# Maintain dependencies for Cargo
- package-ecosystem: "cargo"
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
# Maintain dependencies for GitHub Actions
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10

View File

@@ -0,0 +1,52 @@
#!/bin/bash
# Exit on error. Append "|| true" if you expect an error.
set -o errexit
# Exit on error inside any functions or subshells.
set -o errtrace
# Do not allow use of undefined vars. Use ${VAR:-} to use an undefined VAR
set -o nounset
# Catch the error in case mysqldump fails (but gzip succeeds) in `mysqldump |gzip`
set -o pipefail
# Turn on traces, useful while debugging but commented out by default
# set -o xtrace
last_release="$(git tag --sort=committerdate | grep -E "v0\.\d+\.\d+$" | tail -1)"
echo "🐭 Last release: ${last_release}"
# detect breaking changes
if git log --oneline ${last_release}..HEAD | grep -q '!:' || true; then
echo "🐭 Breaking changes detected since ${last_release}"
git log --oneline ${last_release}..HEAD | grep '!:'
# increment the minor version
minor="${last_release##v0.}"
minor="${minor%.*}"
next_minor="$((minor + 1))"
next_release="v0.${next_minor}.0"
else
# increment the patch version
patch="${last_release##*.}"
next_patch="$((patch + 1))"
next_release="${last_release/%${patch}/${next_patch}}"
fi
echo "🐭 Next release: ${next_release}"
suffix="alpha"
last_tag="$(git tag --sort=committerdate | tail -1)"
if [[ "${last_tag}" = "${next-release}-${suffix}"* ]]; then
echo "🐭 Last alpha release: ${last_tag}"
# increment the alpha version
# e.g. v0.22.1-alpha.12 -> v0.22.1-alpha.13
alpha="${last_tag##*-${suffix}.}"
next_alpha="$((alpha + 1))"
next_tag="${last_tag/%${alpha}/${next_alpha}}"
else
# increment the patch and start the alpha version from 0
# e.g. v0.22.0 -> v0.22.1-alpha.0
next_tag="${next_release}-${suffix}.0"
fi
# update the crate version
msg="# crate version"
sed -E -i "s/^version = .* ${msg}$/version = \"${next_tag#v}\" ${msg}/" Cargo.toml
echo "NEXT_TAG=${next_tag}" >> $GITHUB_ENV
echo "🐭 Next alpha release: ${next_tag}"

View File

@@ -23,32 +23,12 @@ jobs:
if: ${{ !startsWith(github.event.ref, 'refs/tags/v') }}
steps:
- name: Checkout the repository
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Calculate the next release
run: |
suffix="alpha"
last_tag="$(git tag --sort=committerdate | tail -1)"
if [[ "${last_tag}" = *"-${suffix}"* ]]; then
# increment the alpha version
# e.g. v0.22.1-alpha.12 -> v0.22.1-alpha.13
alpha="${last_tag##*-${suffix}.}"
next_alpha="$((alpha + 1))"
next_tag="${last_tag/%${alpha}/${next_alpha}}"
else
# increment the patch and start the alpha version from 0
# e.g. v0.22.0 -> v0.22.1-alpha.0
patch="${last_tag##*.}"
next_patch="$((patch + 1))"
next_tag="${last_tag/%${patch}/${next_patch}}-${suffix}.0"
fi
# update the crate version
msg="# crate version"
sed -E -i "s/^version = .* ${msg}$/version = \"${next_tag#v}\" ${msg}/" Cargo.toml
echo "NEXT_TAG=${next_tag}" >> $GITHUB_ENV
echo "Next alpha release: ${next_tag} 🐭"
run: .github/workflows/calculate-alpha-release.bash
- name: Publish on crates.io
uses: actions-rs/cargo@v1
@@ -77,7 +57,7 @@ jobs:
if: ${{ startsWith(github.event.ref, 'refs/tags/v') }}
steps:
- name: Checkout the repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Publish on crates.io
uses: actions-rs/cargo@v1

87
.github/workflows/check-pr.yml vendored Normal file
View File

@@ -0,0 +1,87 @@
name: Check Pull Requests
on:
pull_request_target:
types:
- opened
- edited
- synchronize
- labeled
- unlabeled
merge_group:
permissions:
pull-requests: write
jobs:
check-title:
runs-on: ubuntu-latest
steps:
- name: Check PR title
if: github.event_name == 'pull_request_target'
uses: amannn/action-semantic-pull-request@v5
id: check_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Add comment indicating we require pull request titles to follow conventional commits specification
- uses: marocchino/sticky-pull-request-comment@v2
if: always() && (steps.check_pr_title.outputs.error_message != null)
with:
header: pr-title-lint-error
message: |
Thank you for opening this pull request!
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
Details:
> ${{ steps.check_pr_title.outputs.error_message }}
# Delete a previous comment when the issue has been resolved
- if: ${{ steps.check_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@v2
with:
header: pr-title-lint-error
delete: true
check-breaking-change-label:
runs-on: ubuntu-latest
env:
# use an environment variable to pass untrusted input to the script
# see https://securitylab.github.com/research/github-actions-untrusted-input/
PR_TITLE: ${{ github.event.pull_request.title }}
steps:
- name: Check breaking change label
id: check_breaking_change
run: |
pattern='^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\(\w+\))?!:'
# Check if pattern matches
if echo "${PR_TITLE}" | grep -qE "$pattern"; then
echo "breaking_change=true" >> $GITHUB_OUTPUT
else
echo "breaking_change=false" >> $GITHUB_OUTPUT
fi
- name: Add label
if: steps.check_breaking_change.outputs.breaking_change == 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['breaking change']
})
do-not-merge:
if: ${{ contains(github.event.*.labels.*.name, 'do not merge') }}
name: Prevent Merging
runs-on: ubuntu-latest
steps:
- name: Check for label
run: |
echo "Pull request is labeled as 'do not merge'"
echo "This workflow fails so that the pull request cannot be merged"
exit 1

View File

@@ -30,12 +30,22 @@ jobs:
steps:
- name: Checkout
if: github.event_name != 'pull_request'
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Checkout
if: github.event_name == 'pull_request'
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Check formatting
run: cargo make lint-format
- name: Check documentation
run: cargo make lint-docs
- name: Check conventional commits
uses: crate-ci/committed@master
with:
@@ -45,20 +55,12 @@ jobs:
uses: crate-ci/typos@master
- name: Lint dependencies
uses: EmbarkStudios/cargo-deny-action@v1
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Check formatting
run: cargo make fmt
clippy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
@@ -72,7 +74,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
@@ -94,11 +96,11 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
toolchain: [ "1.67.0", "stable" ]
toolchain: [ "1.70.0", "stable" ]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Rust {{ matrix.toolchain }}
uses: dtolnay/rust-toolchain@master
with:
@@ -118,7 +120,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Install cargo-make
@@ -133,7 +135,7 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
toolchain: [ "1.67.0", "stable" ]
toolchain: [ "1.70.0", "stable" ]
backend: [ crossterm, termion, termwiz ]
exclude:
# termion is not supported on windows
@@ -142,13 +144,15 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Rust ${{ matrix.toolchain }}}
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Install cargo-make
uses: taiki-e/install-action@nextest
- name: Test ${{ matrix.backend }}
run: cargo make test-backend ${{ matrix.backend }}
env:

View File

@@ -1,10 +1,13 @@
# configuration for https://github.com/DavidAnson/markdownlint
first-line-heading: false
no-inline-html:
allowed_elements:
- img
- details
- summary
- div
- br
line-length:
line_length: 100

376
BREAKING-CHANGES.md Normal file
View File

@@ -0,0 +1,376 @@
# Breaking Changes
This document contains a list of breaking changes in each version and some notes to help migrate
between versions. It is compiled manually from the commit history and changelog. We also tag PRs on
github with a [breaking change] label.
[breaking change]: (https://github.com/ratatui-org/ratatui/issues?q=label%3A%22breaking+change%22)
## Summary
This is a quick summary of the sections below:
- [v0.26.0 (unreleased)](#v0260-unreleased)
- `Line` now has an extra `style` field which applies the style to the entire line
- `Block` style methods cannot be created in a const context
- [v0.25.0](#v0250)
- Removed `Axis::title_style` and `Buffer::set_background`
- `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>`
- `Table::new()` now requires specifying the widths
- `Table::widths()` now accepts `IntoIterator<Item = AsRef<Constraint>>`
- Layout::new() now accepts direction and constraint parameters
- The default `Tabs::highlight_style` is now `Style::new().reversed()`
- [v0.24.0](#v0240)
- MSRV is now 1.70.0
- `ScrollbarState`: `position`, `content_length`, and `viewport_content_length` are now `usize`
- `BorderType`: `line_symbols` is now `border_symbols` and returns `symbols::border::set`
- `Frame<'a, B: Backend>` is now `Frame<'a>`
- `Stylize` shorthands for `String` now consume the value and return `Span<'static>`
- `Spans` is removed
- [v0.23.0](#v0230)
- `Scrollbar`: `track_symbol` now takes `Option<&str>`
- `Scrollbar`: symbols moved to `symbols` module
- MSRV is now 1.67.0
- [v0.22.0](#v0220)
- serde representation of `Borders` and `Modifiers` has changed
- [v0.21.0](#v0210)
- MSRV is now 1.65.0
- `terminal::ViewPort` is now an enum
- `"".as_ref()` must be annotated to implement `Into<Text<'a>>`
- `Marker::Block` renders as a block char instead of a bar char
- [v0.20.0](#v0200)
- MSRV is now 1.63.0
- `List` no longer ignores empty strings
## v0.26.0 (unreleased)
### `Block` style methods cannot be used in a const context ([#720])
[#720]: https://github.com/ratatui-org/ratatui/pull/720
Previously the `style()`, `border_style()` and `title_style()` methods could be used to create a
`Block` in a constant context. These now accept `Into<Style>` instead of `Style`. These methods no
longer can be called from a constant context.
### `Line` now has a `style` field that applies to the entire line ([#708])
[#708]: https://github.com/ratatui-org/ratatui/pull/708
Previously the style of a `Line` was stored in the `Span`s that make up the line. Now the `Line`
itself has a `style` field, which can be set with the `Line::style` method. Any code that creates
`Line`s using the struct initializer instead of constructors will fail to compile due to the added
field. This can be easily fixed by adding `..Default::default()` to the field list or by using a
constructor method (`Line::styled()`, `Line::raw()`) or conversion method (`Line::from()`).
Each `Span` contained within the line will no longer have the style that is applied to the line in
the `Span::style` field.
```diff
let line = Line {
spans: vec!["".into()],
alignment: Alignment::Left,
+ ..Default::default()
};
// or
let line = Line::raw(vec!["".into()])
.alignment(Alignment::Left);
```
## [v0.25.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.25.0)
### Removed `Axis::title_style` and `Buffer::set_background` ([#691])
[#691]: https://github.com/ratatui-org/ratatui/pull/691
These items were deprecated since 0.10.
- You should use styling capabilities of [`text::Line`] given as argument of [`Axis::title`]
instead of `Axis::title_style`
- You should use styling capabilities of [`Buffer::set_style`] instead of `Buffer::set_background`
[`text::Line`]: https://docs.rs/ratatui/latest/ratatui/text/struct.Line.html
[`Axis::title`]: https://docs.rs/ratatui/latest/ratatui/widgets/struct.Axis.html#method.title
[`Buffer::set_style`]: https://docs.rs/ratatui/latest/ratatui/buffer/struct.Buffer.html#method.set_style
### `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>` ([#672])
[#672]: https://github.com/ratatui-org/ratatui/pull/672
Previously `List::new()` took `Into<Vec<ListItem<'a>>>`. This change will throw a compilation
error for `IntoIterator`s with an indeterminate item (e.g. empty vecs).
E.g.
```diff
- let list = List::new(vec![]);
// becomes
+ let list = List::default();
```
### The default `Tabs::highlight_style` is now `Style::new().reversed()` ([#635])
[#635]: https://github.com/ratatui-org/ratatui/pull/635
Previously the default highlight style for tabs was `Style::default()`, which meant that a `Tabs`
widget in the default configuration would not show any indication of the selected tab.
### The default `Tabs::highlight_style` is now `Style::new().reversed()` ([#635])
Previously the default highlight style for tabs was `Style::default()`, which meant that a `Tabs`
widget in the default configuration would not show any indication of the selected tab.
### `Table::new()` now requires specifying the widths of the columns ([#664])
[#664]: https://github.com/ratatui-org/ratatui/pull/664
Previously `Table`s could be constructed without widths. In almost all cases this is an error.
A new widths parameter is now mandatory on `Table::new()`. Existing code of the form:
```diff
- Table::new(rows).widths(widths)
```
Should be updated to:
```diff
+ Table::new(rows, widths)
```
For ease of automated replacement in cases where the amount of code broken by this change is large
or complex, it may be convenient to replace `Table::new` with `Table::default().rows`.
```diff
- Table::new(rows).block(block).widths(widths);
// becomes
+ Table::default().rows(rows).widths(widths)
```
### `Table::widths()` now accepts `IntoIterator<Item = AsRef<Constraint>>` ([#663])
[#663]: https://github.com/ratatui-org/ratatui/pull/663
Previously `Table::widths()` took a slice (`&'a [Constraint]`). This change will introduce clippy
`needless_borrow` warnings for places where slices are passed to this method. To fix these, remove
the `&`.
E.g.
```diff
- let table = Table::new(rows).widths(&[Constraint::Length(1)]);
// becomes
+ let table = Table::new(rows, [Constraint::Length(1)]);
```
### Layout::new() now accepts direction and constraint parameters ([#557])
[#557]: https://github.com/ratatui-org/ratatui/pull/557
Previously layout new took no parameters. Existing code should either use `Layout::default()` or
the new constructor.
```rust
let layout = layout::new()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Max(2)]);
// becomes either
let layout = layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Max(2)]);
// or
let layout = layout::new(Direction::Vertical, [Constraint::Min(1), Constraint::Max(2)]);
```
## [v0.24.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.24.0)
### ScrollbarState field type changed from `u16` to `usize` ([#456])
[#456]: https://github.com/ratatui-org/ratatui/pull/456
In order to support larger content lengths, the `position`, `content_length` and
`viewport_content_length` methods on `ScrollbarState` now take `usize` instead of `u16`
### `BorderType::line_symbols` renamed to `border_symbols` ([#529])
[#529]: https://github.com/ratatui-org/ratatui/issues/529
Applications can now set custom borders on a `Block` by calling `border_set()`. The
`BorderType::line_symbols()` is renamed to `border_symbols()` and now returns a new struct
`symbols::border::Set`. E.g.:
```diff
- let line_set: symbols::line::Set = BorderType::line_symbols(BorderType::Plain);
// becomes
+ let border_set: symbols::border::Set = BorderType::border_symbols(BorderType::Plain);
```
### Generic `Backend` parameter removed from `Frame` ([#530])
[#530]: https://github.com/ratatui-org/ratatui/issues/530
`Frame` is no longer generic over Backend. Code that accepted `Frame<Backend>` will now need to
accept `Frame`. To migrate existing code, remove any generic parameters from code that uses an
instance of a Frame. E.g.:
```diff
- fn ui<B: Backend>(frame: &mut Frame<B>) { ... }
// becomes
+ fn ui(frame: Frame) { ... }
```
### `Stylize` shorthands now consume rather than borrow `String` ([#466])
[#466]: https://github.com/ratatui-org/ratatui/issues/466
In order to support using `Stylize` shorthands (e.g. `"foo".red()`) on temporary `String` values, a
new implementation of `Stylize` was added that returns a `Span<'static>`. This causes the value to
be consumed rather than borrowed. Existing code that expects to use the string after a call will no
longer compile. E.g.
```diff
- let s = String::new("foo");
- let span1 = s.red();
- let span2 = s.blue(); // will no longer compile as s is consumed by the previous line
// becomes
+ let span1 = s.clone().red();
+ let span2 = s.blue();
```
### Deprecated `Spans` type removed (replaced with `Line`) ([#426])
[#426]: https://github.com/ratatui-org/ratatui/issues/426
`Spans` was replaced with `Line` in 0.21.0. `Buffer::set_spans` was replaced with
`Buffer::set_line`.
```diff
- let spans = Spans::from(some_string_str_span_or_vec_span);
- buffer.set_spans(0, 0, spans, 10);
// becomes
+ let line - Line::from(some_string_str_span_or_vec_span);
+ buffer.set_line(0, 0, line, 10);
```
## [v0.23.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.23.0)
### `Scrollbar::track_symbol()` now takes an `Option<&str>` instead of `&str` ([#360])
[#360]: https://github.com/ratatui-org/ratatui/issues/360
The track symbol of `Scrollbar` is now optional, this method now takes an optional value.
```diff
- let scrollbar = Scrollbar::default().track_symbol("|");
// becomes
+ let scrollbar = Scrollbar::default().track_symbol(Some("|"));
```
### `Scrollbar` symbols moved to `symbols::scrollbar` and `widgets::scrollbar` module is private ([#330])
[#330]: https://github.com/ratatui-org/ratatui/issues/330
The symbols for defining scrollbars have been moved to the `symbols` module from the
`widgets::scrollbar` module which is no longer public. To update your code update any imports to the
new module locations. E.g.:
```diff
- use ratatui::{widgets::scrollbar::{Scrollbar, Set}};
// becomes
+ use ratatui::{widgets::Scrollbar, symbols::scrollbar::Set}
```
### MSRV updated to 1.67 ([#361])
[#361]: https://github.com/ratatui-org/ratatui/issues/361
The MSRV of ratatui is now 1.67 due to an MSRV update in a dependency (`time`).
## [v0.22.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.22.0)
### bitflags updated to 2.3 ([#205])
[#205]: https://github.com/ratatui-org/ratatui/issues/205
The serde representation of bitflags has changed. Any existing serialized types that have Borders or
Modifiers will need to be re-serialized. This is documented in the [bitflags
changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md#200-rc2)..
## [v0.21.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.21.0)
### MSRV is 1.65.0 ([#171])
[#171]: https://github.com/ratatui-org/ratatui/issues/171
The minimum supported rust version is now 1.65.0.
### `Terminal::with_options()` stabilized to allow configuring the viewport ([#114])
[#114]: https://github.com/ratatui-org/ratatui/issues/114
In order to support inline viewports, the unstable method `Terminal::with_options()` was stabilized
and `ViewPort` was changed from a struct to an enum.
```diff
let terminal = Terminal::with_options(backend, TerminalOptions {
- viewport: Viewport::fixed(area),
});
// becomes
let terminal = Terminal::with_options(backend, TerminalOptions {
+ viewport: Viewport::Fixed(area),
});
```
### Code that binds `Into<Text<'a>>` now requires type annotations ([#168])
[#168]: https://github.com/ratatui-org/ratatui/issues/168
A new type `Masked` was introduced that implements `From<Text<'a>>`. This causes any code that did
previously did not need to use type annotations to fail to compile. To fix this, annotate or call
to_string() / to_owned() / as_str() on the value. E.g.:
```diff
- let paragraph = Paragraph::new("".as_ref());
// becomes
+ let paragraph = Paragraph::new("".as_str());
```
### `Marker::Block` now renders as a block rather than a bar character ([#133])
[#133]: https://github.com/ratatui-org/ratatui/issues/133
Code using the `Block` marker that previously rendered using a half block character (`'▀'``) now
renders using the full block character (`'█'`). A new marker variant`Bar` is introduced to replace
the existing code.
```diff
- let canvas = Canvas::default().marker(Marker::Block);
// becomes
+ let canvas = Canvas::default().marker(Marker::Bar);
```
## [v0.20.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.20.0)
v0.20.0 was the first release of Ratatui - versions prior to this were release as tui-rs. See the
[Changelog](./CHANGELOG.md) for more details.
### MSRV is update to 1.63.0 ([#80])
[#80]: https://github.com/ratatui-org/ratatui/issues/80
The minimum supported rust version is 1.63.0
### List no longer ignores empty string in items ([#42])
[#42]: https://github.com/ratatui-org/ratatui/issues/42
The following code now renders 3 items instead of 2. Code which relies on the previous behavior will
need to manually filter empty items prior to display.
```rust
let items = vec![
ListItem::new("line one"),
ListItem::new(""),
ListItem::new("line four"),
];
```

File diff suppressed because it is too large Load Diff

View File

@@ -29,6 +29,11 @@ change becomes a place where a bug may have been introduced. Consider splitting
reformatting changes into a separate PR from those that make a behavioral change, as the tests help
guarantee that the behavior is unchanged.
### Code formatting
Run `cargo make format` before committing to ensure that code is consistently formatted with
rustfmt. Configuration is in [rustfmt.toml](./rustfmt.toml).
### Search `tui-rs` for similar work
The original fork of Ratatui, [`tui-rs`](https://github.com/fdehau/tui-rs/), has a large amount of
@@ -113,6 +118,59 @@ exist to show coverage directly in your editor. E.g.:
- <https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters>
- <https://github.com/alepez/vim-llvmcov>
### Documentation
Here are some guidelines for writing documentation in Ratatui.
Every public API **must** be documented.
Keep in mind that Ratatui tends to attract beginner Rust users that may not be familiar with Rust
concepts.
#### Content
The main doc comment should talk about the general features that the widget supports and introduce
the concepts pointing to the various methods. Focus on interaction with various features and giving
enough information that helps understand why you might want something.
Examples should help users understand a particular usage, not test a feature. They should be as
simple as possible.
Prefer hiding imports and using wildcards to keep things concise. Some imports may still be shown
to demonstrate a particular non-obvious import (e.g. `Stylize` trait to use style methods).
Speaking of `Stylize`, you should use it over the more verbose style setters:
```rust
let style = Style::new().red().bold();
// not
let style = Style::default().fg(Color::Red).add_modifier(Modifiers::BOLD);
```
#### Format
- First line is summary, second is blank, third onward is more detail
```rust
/// Summary
///
/// A detailed description
/// with examples.
fn foo() {}
```
- Max line length is 100 characters
See [vscode rewrap extension](https://marketplace.visualstudio.com/items?itemName=stkb.rewrap)
- Doc comments are above macros
i.e.
```rust
/// doc comment
#[derive(Debug)]
struct Foo {}
```
- Code items should be between backticks
i.e. ``[`Block`]``, **NOT** ``[Block]``
### Use of unsafe for optimization purposes
We don't currently use any unsafe code in Ratatui, and would like to keep it that way. However there

View File

@@ -1,6 +1,6 @@
[package]
name = "ratatui"
version = "0.23.0" # crate version
version = "0.25.0" # crate version
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
description = "A library that's all about cooking up terminal user interfaces"
documentation = "https://docs.rs/ratatui/latest/ratatui/"
@@ -18,46 +18,58 @@ exclude = [
]
autoexamples = true
edition = "2021"
rust-version = "1.67.0"
rust-version = "1.70.0"
[badges]
[dependencies]
#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.
#!
#! Generally an application will only use one backend, so you should only enable one of the following features:
## enables the [`CrosstermBackend`] backend and adds a dependency on the [Crossterm crate].
crossterm = { version = "0.27", optional = true }
## enables the [`TermionBackend`] backend and adds a dependency on the [Termion crate].
termion = { version = "2.0", optional = true }
## enables the [`TermwizBackend`] backend and adds a dependency on the [Termwiz crate].
termwiz = { version = "0.20.0", optional = true }
serde = { version = "1", optional = true, features = ["derive"] }
bitflags = "2.3"
cassowary = "0.3"
indoc = "2.0"
itertools = "0.11"
itertools = "0.12"
paste = "1.0.2"
strum = { version = "0.25", features = ["derive"] }
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
unicode-segmentation = "1.10"
unicode-width = "0.1"
document-features = { version = "0.2.7", optional = true }
lru = "0.12.0"
stability = "0.1.1"
[dev-dependencies]
anyhow = "1.0.71"
argh = "0.1"
argh = "0.1.12"
better-panic = "0.3.0"
cargo-husky = { version = "1.5.0", default-features = false, features = [
"user-hooks",
] }
criterion = { version = "0.5", features = ["html_reports"] }
color-eyre = "0.6.2"
criterion = { version = "0.5.1", features = ["html_reports"] }
fakeit = "1.1"
rand = "0.8"
palette = "0.7.3"
pretty_assertions = "1.4.0"
rand = "0.8.5"
[features]
default = ["crossterm"]
#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.
#!
## By default, we enable the crossterm backend as this is a reasonable choice for most applications
## as it is supported on Linux/Mac/Windows systems. We also enable the `underline-color` feature
## which allows you to set the underline color of text.
default = ["crossterm", "underline-color"]
#! Generally an application will only use one backend, so you should only enable one of the following features:
## enables the [`CrosstermBackend`] backend and adds a dependency on the [Crossterm crate].
crossterm = ["dep:crossterm"]
## enables the [`TermionBackend`] backend and adds a dependency on the [Termion crate].
termion = ["dep:termion"]
## enables the [`TermwizBackend`] backend and adds a dependency on the [Termwiz crate].
termwiz = ["dep:termwiz"]
#! The following optional features are available for all backends:
## enables serialization and deserialization of style and color types using the [Serde crate].
## This is useful if you want to save themes to a file.
@@ -74,16 +86,47 @@ all-widgets = ["widget-calendar"]
## enables the [`calendar`] widget module and adds a dependency on the [Time crate].
widget-calendar = ["dep:time"]
#! Underline color is only supported by the [`CrosstermBackend`] backend, and is not supported
#! on Windows 7.
## enables the backend code that sets the underline color.
underline-color = ["dep:crossterm"]
#! The following features are unstable and may change in the future:
## Enable all unstable features.
unstable = ["unstable-segment-size", "unstable-rendered-line-info"]
## Enables the [`Layout::segment_size`](crate::layout::Layout::segment_size) method which is experimental and may change in the
## future. See [Issue #536](https://github.com/ratatui-org/ratatui/issues/536) for more details.
unstable-segment-size = []
## Enables the [`Paragraph::line_count`](crate::widgets::Paragraph::line_count)
## [`Paragraph::line_width`](crate::widgets::Paragraph::line_width) methods
## which are experimental and may change in the future.
## See [Issue 293](https://github.com/ratatui-org/ratatui/issues/293) for more details.
unstable-rendered-line-info = []
[package.metadata.docs.rs]
all-features = true
# see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
rustdoc-args = ["--cfg", "docsrs"]
[[bench]]
name = "barchart"
harness = false
[[bench]]
name = "block"
harness = false
[[bench]]
name = "list"
harness = false
[lib]
bench = false
[[bench]]
name = "paragraph"
harness = false
@@ -92,10 +135,6 @@ harness = false
name = "sparkline"
harness = false
[[bench]]
name = "list"
harness = false
[[example]]
name = "barchart"
@@ -128,6 +167,11 @@ required-features = ["crossterm"]
# this example is a bit verbose, so we don't want to include it in the docs
doc-scrape-examples = false
[[example]]
name = "colors_rgb"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "custom_widget"
required-features = ["crossterm"]
@@ -138,6 +182,16 @@ name = "demo"
# this runs for all of the terminal backends, so it can't be built using --all-features or scraped
doc-scrape-examples = false
[[example]]
name = "demo2"
required-features = ["crossterm", "widget-calendar"]
doc-scrape-examples = true
[[example]]
name = "docsrs"
required-features = ["crossterm"]
doc-scrape-examples = false
[[example]]
name = "gauge"
required-features = ["crossterm"]
@@ -179,6 +233,11 @@ name = "popup"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "ratatui-logo"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "scrollbar"
required-features = ["crossterm"]

View File

@@ -7,47 +7,62 @@ skip_core_tasks = true
# all features except the backend ones
ALL_FEATURES = "all-widgets,macros,serde"
# Windows does not support building termion, so this avoids the build failure by providing two
# sets of flags, one for Windows and one for other platforms.
# Windows: --features=all-widgets,macros,serde,crossterm,termwiz,underline-color
# Other: --features=all-widgets,macros,serde,crossterm,termion,termwiz,underline-color
ALL_FEATURES_FLAG = { source = "${CARGO_MAKE_RUST_TARGET_OS}", default_value = "--features=all-widgets,macros,serde,crossterm,termion,termwiz,unstable", mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz,unstable" } }
[tasks.default]
alias = "ci"
[tasks.ci]
description = "Run continuous integration tasks"
dependencies = [
"style-check",
"clippy",
"check",
"test",
]
dependencies = ["lint-style", "clippy", "check", "test"]
[tasks.style-check]
description = "Check code style"
dependencies = ["fmt", "typos"]
[tasks.lint-style]
description = "Lint code style (formatting, typos, docs)"
dependencies = ["lint-format", "lint-typos", "lint-docs"]
[tasks.fmt]
description = "Format source code"
[tasks.lint-format]
description = "Lint code formatting"
toolchain = "nightly"
command = "cargo"
args = ["fmt", "--all", "--check"]
[tasks.typos]
[tasks.format]
description = "Fix code formatting"
toolchain = "nightly"
command = "cargo"
args = ["fmt", "--all"]
[tasks.lint-typos]
description = "Run typo checks"
install_crate = { crate_name = "typos-cli", binary = "typos", test_arg = "--version" }
command = "typos"
[tasks.lint-docs]
description = "Check documentation for errors and warnings"
toolchain = "nightly"
command = "cargo"
args = [
"rustdoc",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
"--",
"-Zunstable-options",
"--check",
"-Dwarnings",
]
[tasks.check]
description = "Check code for errors and warnings"
command = "cargo"
args = [
"check",
"--all-targets",
"--all-features"
]
[tasks.check.windows]
args = [
"check",
"--all-targets",
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz"
"--no-default-features",
"${ALL_FEATURES_FLAG}",
]
[tasks.build]
@@ -56,14 +71,8 @@ command = "cargo"
args = [
"build",
"--all-targets",
"--all-features",
]
[tasks.build.windows]
args = [
"build",
"--all-targets",
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz"
"--no-default-features",
"${ALL_FEATURES_FLAG}",
]
[tasks.clippy]
@@ -74,98 +83,76 @@ args = [
"--all-targets",
"--tests",
"--benches",
"--all-features",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
"--",
"-D",
"warnings",
]
[tasks.clippy.windows]
args = [
"clippy",
"--all-targets",
"--tests",
"--benches",
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz",
"--",
"-D",
"warnings",
]
[tasks.install-nextest]
description = "Install cargo-nextest"
install_crate = { crate_name = "cargo-nextest", binary = "cargo-nextest", test_arg = "--help" }
[tasks.test]
description = "Run tests"
dependencies = [
"test-doc",
]
run_task = { name = ["test-lib", "test-doc"] }
[tasks.test-lib]
description = "Run default tests"
dependencies = ["install-nextest"]
command = "cargo"
args = [
"test",
"nextest",
"run",
"--all-targets",
"--all-features",
]
[tasks.test-windows]
description = "Run tests on Windows"
dependencies = [
"test-doc",
]
args = [
"test",
"--all-targets",
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz"
"--no-default-features",
"${ALL_FEATURES_FLAG}",
]
[tasks.test-doc]
description = "Run documentation tests"
command = "cargo"
args = [
"test", "--doc",
"--all-features",
]
[tasks.test-doc.windows]
args = [
"test", "--doc",
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz"
]
args = ["test", "--doc", "--no-default-features", "${ALL_FEATURES_FLAG}"]
[tasks.test-backend]
# takes a command line parameter to specify the backend to test (e.g. "crossterm")
description = "Run backend-specific tests"
dependencies = ["install-nextest"]
command = "cargo"
args = [
"test",
"nextest",
"run",
"--all-targets",
"--no-default-features", "--features", "${ALL_FEATURES},${@}"
"--no-default-features",
"--features",
"${ALL_FEATURES},${@}",
]
[tasks.coverage]
description = "Generate code coverage report"
command = "cargo"
args = [
"llvm-cov",
"--lcov",
"--output-path", "target/lcov.info",
"--all-features",
]
[tasks.coverage.windows]
command = "cargo"
args = [
"llvm-cov",
"--lcov",
"--output-path", "target/lcov.info",
"--output-path",
"target/lcov.info",
"--no-default-features",
"--features", "${ALL_FEATURES},crossterm,termwiz",
"${ALL_FEATURES_FLAG}",
]
[tasks.run-example]
private = true
condition = { env_set = ["TUI_EXAMPLE_NAME"] }
command = "cargo"
args = ["run", "--release", "--example", "${TUI_EXAMPLE_NAME}", "--features", "all-widgets"]
args = [
"run",
"--release",
"--example",
"${TUI_EXAMPLE_NAME}",
"--features",
"all-widgets",
]
[tasks.build-examples]
description = "Compile project examples"

519
README.md
View File

@@ -1,138 +1,350 @@
# Ratatui
<img align="left" src="https://avatars.githubusercontent.com/u/125200832?s=128&v=4">
`ratatui` is a [Rust](https://www.rust-lang.org) library that is all about cooking up terminal user interfaces.
It is a community fork of the original [tui-rs](https://github.com/fdehau/tui-rs)
project.
[![Crates.io](https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square)](https://crates.io/crates/ratatui)
[![License](https://img.shields.io/crates/l/ratatui?style=flat-square)](./LICENSE) [![GitHub CI
Status](https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui/ci.yml?style=flat-square&logo=github)](https://github.com/ratatui-org/ratatui/actions?query=workflow%3ACI+)
[![Docs.rs](https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square)](https://docs.rs/crate/ratatui/)
[![Dependency
Status](https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square)](https://deps.rs/repo/github/ratatui-org/ratatui)
[![Codecov](https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST)](https://app.codecov.io/gh/ratatui-org/ratatui)
[![Discord](https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square)](https://discord.gg/pMCEU9hNEj)
[![Matrix](https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix)](https://matrix.to/#/#ratatui:matrix.org)
<!-- See RELEASE.md for instructions on creating the demo gif --->
![Demo of Ratatui](https://vhs.charm.sh/vhs-tF0QbuPbtHgUeG0sTVgFr.gif)
<details>
<summary>Table of Contents</summary>
* [Ratatui](#ratatui)
* [Installation](#installation)
* [Introduction](#introduction)
* [Quickstart](#quickstart)
* [Status of this fork](#status-of-this-fork)
* [Rust version requirements](#rust-version-requirements)
* [Documentation](#documentation)
* [Examples](#examples)
* [Widgets](#widgets)
* [Built in](#built-in)
* [Third\-party libraries, bootstrapping templates and
- [Ratatui](#ratatui)
- [Installation](#installation)
- [Introduction](#introduction)
- [Other Documentation](#other-documentation)
- [Quickstart](#quickstart)
- [Status of this fork](#status-of-this-fork)
- [Rust version requirements](#rust-version-requirements)
- [Widgets](#widgets)
- [Built in](#built-in)
- [Third\-party libraries, bootstrapping templates and
widgets](#third-party-libraries-bootstrapping-templates-and-widgets)
* [Apps](#apps)
* [Alternatives](#alternatives)
* [Contributors](#contributors)
* [Acknowledgments](#acknowledgments)
* [License](#license)
- [Apps](#apps)
- [Alternatives](#alternatives)
- [Acknowledgments](#acknowledgments)
- [License](#license)
</details>
<!-- cargo-rdme start -->
![Demo](https://raw.githubusercontent.com/ratatui-org/ratatui/b33c878808c4c40591d7a2d9f9d94d6fee95a96f/examples/demo2.gif)
<div align="center">
[![Crate Badge]](https://crates.io/crates/ratatui)
[![License Badge]](./LICENSE)
[![CI Badge]](https://github.com/ratatui-org/ratatui/actions?query=workflow%3ACI+)
[![Docs Badge]](https://docs.rs/crate/ratatui/)<br>
[![Dependencies Badge]](https://deps.rs/repo/github/ratatui-org/ratatui)
[![Codecov Badge]](https://app.codecov.io/gh/ratatui-org/ratatui)
[![Discord Badge]](https://discord.gg/pMCEU9hNEj)
[![Matrix Badge]](https://matrix.to/#/#ratatui:matrix.org)<br>
[Documentation](https://docs.rs/ratatui)
· [Ratatui Website](https://ratatui.rs)
· [Examples](https://github.com/ratatui-org/ratatui/tree/main/examples)
· [Report a bug](https://github.com/ratatui-org/ratatui/issues/new?labels=bug&projects=&template=bug_report.md)
· [Request a Feature](https://github.com/ratatui-org/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md)
· [Send a Pull Request](https://github.com/ratatui-org/ratatui/compare)
</div>
# Ratatui
[Ratatui] is a crate for cooking up terminal user interfaces in Rust. It is a lightweight
library that provides a set of widgets and utilities to build complex Rust TUIs. Ratatui was
forked from the [tui-rs] crate in 2023 in order to continue its development.
## Installation
Add `ratatui` and `crossterm` as dependencies to your cargo.toml:
```shell
cargo add ratatui --features all-widgets
cargo add ratatui crossterm
```
Or modify your `Cargo.toml`
```toml
[dependencies]
ratatui = { version = "0.23.0", features = ["all-widgets"]}
```
Ratatui is mostly backwards compatible with `tui-rs`. To migrate an existing project, it may be
easier to rename the ratatui dependency to `tui` rather than updating every usage of the crate.
E.g.:
```toml
[dependencies]
tui = { package = "ratatui", version = "0.23.0", features = ["all-widgets"]}
```
Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
section of the [Ratatui Website] for more details on how to use other backends ([Termion] /
[Termwiz]).
## Introduction
`ratatui` is a terminal UI library that supports multiple backends:
Ratatui is based on the principle of immediate rendering with intermediate buffers. This means
that for each frame, your app must render all widgets that are supposed to be part of the UI.
This is in contrast to the retained mode style of rendering where widgets are updated and then
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website] for
more info.
* [crossterm](https://github.com/crossterm-rs/crossterm) [default]
* [termion](https://github.com/ticki/termion)
* [termwiz](https://github.com/wez/wezterm/tree/master/termwiz)
## Other documentation
The library is based on the principle of immediate rendering with intermediate buffers. This means
that at each new frame you should build all widgets that are supposed to be part of the UI. While
providing a great flexibility for rich and interactive UI, this may introduce overhead for highly
dynamic content. So, the implementation try to minimize the number of ansi escapes sequences
generated to draw the updated UI. In practice, given the speed of `Rust` the overhead rather 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.
We keep a [CHANGELOG](./CHANGELOG.md) generated by [git-cliff](https://github.com/orhun/git-cliff)
utilizing [Conventional Commits](https://www.conventionalcommits.org/).
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
- [Examples] - a collection of examples that demonstrate how to use the library.
- [API Documentation] - the full API documentation for the library on docs.rs.
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
- [Contributing] - Please read this if you are interested in contributing to the project.
- [Breaking Changes] - a list of breaking changes in the library.
## Quickstart
The following example demonstrates the minimal amount of code necessary to setup a terminal and
render "Hello World!". The full code for this example which contains a little more detail is in
[hello_world.rs](./examples/hello_world.rs). For more guidance on how to create Ratatui apps, see
the [Docs](https://docs.rs/ratatui) and [Examples](#examples). There is also a starter template
available at [rust-tui-template](https://github.com/ratatui-org/rust-tui-template).
[hello_world.rs]. For more guidance on different ways to structure your application see the
[Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the various
[Examples]. There are also several starter templates available:
- [template]
- [async-template] (book and template)
Every application built with `ratatui` needs to implement the following steps:
- Initialize the terminal
- A main loop to:
- Handle input events
- Draw the UI
- Restore the terminal state
The library contains a [`prelude`] module that re-exports the most commonly used traits and
types for convenience. Most examples in the documentation will use this instead of showing the
full path of each type.
### Initialize and restore the terminal
The [`Terminal`] type is the main entry point for any Ratatui application. It is a light
abstraction over a choice of [`Backend`] implementations that provides functionality to draw
each frame, clear the screen, hide the cursor, etc. It is parametrized over any type that
implements the [`Backend`] trait which has implementations for [Crossterm], [Termion] and
[Termwiz].
Most applications should enter the Alternate Screen when starting and leave it when exiting and
also enable raw mode to disable line buffering and enable reading key events. See the [`backend`
module] and the [Backends] section of the [Ratatui Website] for more info.
### Drawing the UI
The drawing logic is delegated to a closure that takes a [`Frame`] instance as argument. The
[`Frame`] provides the size of the area to draw to and allows the app to render any [`Widget`]
using the provided [`render_widget`] method. See the [Widgets] section of the [Ratatui Website] for
more info.
### Handling events
Ratatui does not include any input handling. Instead event handling can be implemented by
calling backend library methods directly. See the [Handling Events] section of the [Ratatui
Website] for more info. For example, if you are using [Crossterm], you can use the
[`crossterm::event`] module to handle events.
### Example
```rust
fn main() -> Result<(), Box<dyn Error>> {
let mut terminal = setup_terminal()?;
run(&mut terminal)?;
restore_terminal(&mut terminal)?;
use std::io::{self, stdout};
use crossterm::{
event::{self, Event, KeyCode},
ExecutableCommand,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}
};
use ratatui::{prelude::*, widgets::*};
fn main() -> io::Result<()> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
let mut should_quit = false;
while !should_quit {
terminal.draw(ui)?;
should_quit = handle_events()?;
}
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>, Box<dyn Error>> {
let mut stdout = io::stdout();
enable_raw_mode()?;
execute!(stdout, EnterAlternateScreen)?;
Ok(Terminal::new(CrosstermBackend::new(stdout))?)
}
fn restore_terminal(
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
) -> Result<(), Box<dyn Error>> {
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen,)?;
Ok(terminal.show_cursor()?)
}
fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<(), Box<dyn Error>> {
Ok(loop {
terminal.draw(|frame| {
let greeting = Paragraph::new("Hello World!");
frame.render_widget(greeting, frame.size());
})?;
if event::poll(Duration::from_millis(250))? {
if let Event::Key(key) = event::read()? {
if KeyCode::Char('q') == key.code {
break;
}
fn handle_events() -> io::Result<bool> {
if event::poll(std::time::Duration::from_millis(50))? {
if let Event::Key(key) = event::read()? {
if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(true);
}
}
})
}
}
Ok(false)
}
fn ui(frame: &mut Frame) {
frame.render_widget(
Paragraph::new("Hello World!")
.block(Block::default().title("Greeting").borders(Borders::ALL)),
frame.size(),
);
}
```
Running this example produces the following output:
![docsrs-hello]
## Layout
The library comes with a basic yet useful layout management object called [`Layout`] which
allows you to split the available space into multiple areas and then render widgets in each
area. This lets you describe a responsive terminal UI by nesting layouts. See the [Layout]
section of the [Ratatui Website] for more info.
```rust
use ratatui::{prelude::*, widgets::*};
fn ui(frame: &mut Frame) {
let main_layout = Layout::new(
Direction::Vertical,
[
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
]
)
.split(frame.size());
frame.render_widget(
Block::new().borders(Borders::TOP).title("Title Bar"),
main_layout[0],
);
frame.render_widget(
Block::new().borders(Borders::TOP).title("Status Bar"),
main_layout[2],
);
let inner_layout = Layout::new(
Direction::Horizontal,
[Constraint::Percentage(50), Constraint::Percentage(50)]
)
.split(main_layout[1]);
frame.render_widget(
Block::default().borders(Borders::ALL).title("Left"),
inner_layout[0],
);
frame.render_widget(
Block::default().borders(Borders::ALL).title("Right"),
inner_layout[1],
);
}
```
Running this example produces the following output:
![docsrs-layout]
## Text and styling
The [`Text`], [`Line`] and [`Span`] types are the building blocks of the library and are used in
many places. [`Text`] is a list of [`Line`]s and a [`Line`] is a list of [`Span`]s. A [`Span`]
is a string with a specific style.
The [`style` module] provides types that represent the various styling options. The most
important one is [`Style`] which represents the foreground and background colors and the text
attributes of a [`Span`]. The [`style` module] also provides a [`Stylize`] trait that allows
short-hand syntax to apply a style to widgets and text. See the [Styling Text] section of the
[Ratatui Website] for more info.
```rust
use ratatui::{prelude::*, widgets::*};
fn ui(frame: &mut Frame) {
let areas = Layout::new(
Direction::Vertical,
[
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
]
)
.split(frame.size());
let span1 = Span::raw("Hello ");
let span2 = Span::styled(
"World",
Style::new()
.fg(Color::Green)
.bg(Color::White)
.add_modifier(Modifier::BOLD),
);
let span3 = "!".red().on_light_yellow().italic();
let line = Line::from(vec![span1, span2, span3]);
let text: Text = Text::from(vec![line]);
frame.render_widget(Paragraph::new(text), areas[0]);
// or using the short-hand syntax and implicit conversions
frame.render_widget(
Paragraph::new("Hello World!".red().on_white().bold()),
areas[1],
);
// to style the whole widget instead of just the text
frame.render_widget(
Paragraph::new("Hello World!").style(Style::new().red().on_white()),
areas[2],
);
// or using the short-hand syntax
frame.render_widget(Paragraph::new("Hello World!").blue().on_yellow(), areas[3]);
}
```
Running this example produces the following output:
![docsrs-styling]
[Ratatui Website]: https://ratatui.rs/
[Installation]: https://ratatui.rs/installation/
[Rendering]: https://ratatui.rs/concepts/rendering/
[Application Patterns]: https://ratatui.rs/concepts/application-patterns/
[Hello World tutorial]: https://ratatui.rs/tutorials/hello-world/
[Backends]: https://ratatui.rs/concepts/backends/
[Widgets]: https://ratatui.rs/how-to/widgets/
[Handling Events]: https://ratatui.rs/concepts/event-handling/
[Layout]: https://ratatui.rs/how-to/layout/
[Styling Text]: https://ratatui.rs/how-to/render/style-text/
[template]: https://github.com/ratatui-org/template
[async-template]: https://ratatui-org.github.io/async-template
[Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples
[git-cliff]: https://git-cliff.org
[Conventional Commits]: https://www.conventionalcommits.org
[API Documentation]: https://docs.rs/ratatui
[Changelog]: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md
[Contributing]: https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md
[Breaking Changes]: https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md
[docsrs-hello]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
[docsrs-layout]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
[docsrs-styling]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
[`Frame`]: terminal::Frame
[`render_widget`]: terminal::Frame::render_widget
[`Widget`]: widgets::Widget
[`Layout`]: layout::Layout
[`Text`]: text::Text
[`Line`]: text::Line
[`Span`]: text::Span
[`Style`]: style::Style
[`style` module]: style
[`Stylize`]: style::Stylize
[`Backend`]: backend::Backend
[`backend` module]: backend
[`crossterm::event`]: https://docs.rs/crossterm/latest/crossterm/event/index.html
[Ratatui]: https://ratatui.rs
[Crossterm]: https://crates.io/crates/crossterm
[Termion]: https://crates.io/crates/termion
[Termwiz]: https://crates.io/crates/termwiz
[tui-rs]: https://crates.io/crates/tui
[hello_world.rs]: https://github.com/ratatui-org/ratatui/blob/main/examples/hello_world.rs
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square
[CI Badge]:
https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui/ci.yml?style=flat-square&logo=github
[Codecov Badge]:
https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST
[Dependencies Badge]: https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square
[Discord Badge]:
https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square
[Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
[Matrix Badge]:
https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix
<!-- cargo-rdme end -->
## Status of this fork
In response to the original maintainer [**Florian Dehau**](https://github.com/fdehau)'s issue
@@ -155,35 +367,6 @@ you are interested in working on a PR or issue opened in the previous repository
Since version 0.23.0, The Minimum Supported Rust Version (MSRV) of `ratatui` is 1.67.0.
## Documentation
The documentation can be found on [docs.rs.](https://docs.rs/ratatui)
## Examples
The demo shown in the gif above is available on all available backends.
```shell
# crossterm
cargo run --example demo
# termion
cargo run --example demo --no-default-features --features=termion
# termwiz
cargo run --example demo --no-default-features --features=termwiz
```
The UI code for this is in [examples/demo/ui.rs](./examples/demo/ui.rs) while the application state
is in [examples/demo/app.rs](./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:
```shell
cargo run --example demo --release -- --tick-rate 200 --enhanced-graphics false
```
More examples are available in the [examples](./examples/) folder.
## Widgets
### Built in
@@ -191,21 +374,21 @@ More examples are available in the [examples](./examples/) folder.
The library comes with the following
[widgets](https://docs.rs/ratatui/latest/ratatui/widgets/index.html):
* [BarChart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
* [Block](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html)
* [Calendar](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
* [Canvas](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/struct.Canvas.html) which allows
- [BarChart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
- [Block](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html)
- [Calendar](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
- [Canvas](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/struct.Canvas.html) which allows
rendering [points, lines, shapes and a world
map](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html)
* [Chart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Chart.html)
* [Clear](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Clear.html)
* [Gauge](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Gauge.html)
* [List](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html)
* [Paragraph](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Paragraph.html)
* [Scrollbar](https://docs.rs/ratatui/latest/ratatui/widgets/scrollbar/struct.Scrollbar.html)
* [Sparkline](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Sparkline.html)
* [Table](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Table.html)
* [Tabs](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Tabs.html)
- [Chart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Chart.html)
- [Clear](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Clear.html)
- [Gauge](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Gauge.html)
- [List](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html)
- [Paragraph](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Paragraph.html)
- [Scrollbar](https://docs.rs/ratatui/latest/ratatui/widgets/scrollbar/struct.Scrollbar.html)
- [Sparkline](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Sparkline.html)
- [Table](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Table.html)
- [Tabs](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Tabs.html)
Each widget has an associated example which can be found in the [examples](./examples/) folder. Run
each examples with cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by
@@ -216,48 +399,42 @@ be installed with `cargo install cargo-make`).
### Third-party libraries, bootstrapping templates and widgets
* [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/ratatui-org/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
- [ansi-to-tui](https://github.com/uttarayan21/ansi-to-tui) — Convert ansi colored text to
`ratatui::text::Text`
- [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
`ratatui::style::Color`
- [rust-tui-template](https://github.com/ratatui-org/rust-tui-template) — A template for
bootstrapping a Rust TUI application with Tui-rs & crossterm
- [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
- [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-treeview](https://github.com/veeso/tui-realm-treeview) — Treeview component for
Tui-realm
* [tui-rs-tree-widgets](https://github.com/EdJoPaTo/tui-rs-tree-widget): Widget for tree data
- [tui-rs-tree-widgets](https://github.com/EdJoPaTo/tui-rs-tree-widget) Widget for tree data
structures.
* [tui-windows](https://github.com/markatk/tui-windows-rs) — Tui-rs abstraction to handle multiple
- [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
- [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-input](https://github.com/sayanarijit/tui-input): TUI input library supporting multiple
- [tui-input](https://github.com/sayanarijit/tui-input) TUI input library supporting multiple
backends and tui-rs.
* [tui-term](https://github.com/a-kenji/tui-term): A pseudoterminal widget library
- [tui-term](https://github.com/a-kenji/tui-term) A pseudoterminal widget library
that enables the rendering of terminal applications as ratatui widgets.
## Apps
Check out the list of more than 50 [Apps using
`Ratatui`](https://github.com/ratatui-org/ratatui/wiki/Apps-using-Ratatui)!
Check out [awesome-ratatui](https://github.com/ratatui-org/awesome-ratatui) for a curated list of
awesome apps/libraries built with `ratatui`!
## Alternatives
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an alternative solution
to build text user interfaces in Rust.
## Contributors
[![GitHub
Contributors](https://contrib.rocks/image?repo=ratatui-org/ratatui)](https://github.com/ratatui-org/ratatui/graphs/contributors)
## Acknowledgments
Special thanks to [**Pavel Fomchenkov**](https://github.com/nawok) for his work in designing **an

View File

@@ -7,19 +7,38 @@ actions](.github/workflows/cd.yml) and triggered by pushing a tag.
[vhs](https://github.com/charmbracelet/vhs) (installation instructions in README).
```shell
cargo build --example demo
vhs examples/demo.tape --publish --quiet
cargo build --example demo2
vhs examples/demo2.tape
```
Then update the link in the [examples README](./examples/README) and the main README. Avoid
adding the gif to the git repo as binary files tend to bloat repositories.
1. Switch branches to the images branch and copy demo2.gif to examples/, commit, and push.
1. Grab the permalink from <https://github.com/ratatui-org/ratatui/blob/images/examples/demo2.gif> and
append `?raw=true` to redirect to the actual image url. Then update the link in the main README.
Avoid adding the gif to the git repo as binary files tend to bloat repositories.
1. Bump the version in [Cargo.toml](Cargo.toml).
1. Bump versions in the doc comments of [lib.rs](src/lib.rs).
1. Ensure [CHANGELOG.md](CHANGELOG.md) is updated. [git-cliff](https://github.com/orhun/git-cliff)
can be used for generating the entries.
1. Ensure that any breaking changes are documented in [BREAKING-CHANGES.md](./BREAKING-CHANGES.md)
1. Commit and push the changes.
1. Create a new tag: `git tag -a v[X.Y.Z]`
1. Push the tag: `git push --tags`
1. Wait for [Continuous Deployment](https://github.com/ratatui-org/ratatui/actions) workflow to
finish.
## Alpha Releases
Alpha releases are automatically released every Saturday via [cd.yml](./.github/workflows/cd.yml)
and can be manually be created when necessary by triggering the [Continuous
Deployment](https://github.com/ratatui-org/ratatui/actions/workflows/cd.yml) workflow.
We automatically release an alpha release with a patch level bump + alpha.num weekly (and when we
need to manually). E.g. the last release was 0.22.0, and the most recent alpha release is
0.22.1-alpha.1.
These releases will have whatever happened to be in main at the time of release, so they're useful
for apps that need to get releases from crates.io, but may contain more bugs and be generally less
tested than normal releases.
See [#147](https://github.com/ratatui-org/ratatui/issues/147) and
[#359](https://github.com/ratatui-org/ratatui/pull/359) for more info on the alpha release process.

9
SECURITY.md Normal file
View File

@@ -0,0 +1,9 @@
# Security Policy
## Supported Versions
We only support the latest version of this crate.
## Reporting a Vulnerability
To report secuirity vulnerability, please use the form at https://github.com/ratatui-org/ratatui/security/advisories/new

BIN
assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

73
benches/barchart.rs Normal file
View File

@@ -0,0 +1,73 @@
use criterion::{criterion_group, criterion_main, Bencher, BenchmarkId, Criterion};
use rand::Rng;
use ratatui::{
buffer::Buffer,
layout::Rect,
prelude::Direction,
widgets::{Bar, BarChart, BarGroup, Widget},
};
/// Benchmark for rendering a barchart.
pub fn barchart(c: &mut Criterion) {
let mut group = c.benchmark_group("barchart");
let mut rng = rand::thread_rng();
for data_count in [64, 256, 2048] {
let data: Vec<Bar> = (0..data_count)
.map(|i| {
Bar::default()
.label(format!("B{i}").into())
.value(rng.gen_range(0..data_count))
})
.collect();
let bargroup = BarGroup::default().bars(&data);
// Render a basic barchart
group.bench_with_input(
BenchmarkId::new("render", data_count),
&BarChart::default().data(bargroup.clone()),
render,
);
// Render an horizontal barchart
group.bench_with_input(
BenchmarkId::new("render_horizontal", data_count),
&BarChart::default()
.direction(Direction::Horizontal)
.data(bargroup.clone()),
render,
);
// Render a barchart with multiple groups
group.bench_with_input(
BenchmarkId::new("render_grouped", data_count),
&BarChart::default()
// We call `data` multiple time to add multiple groups.
// This is not a duplicated call.
.data(bargroup.clone())
.data(bargroup.clone())
.data(bargroup.clone()),
render,
);
}
group.finish();
}
/// Render the widget in a classical size buffer
fn render(bencher: &mut Bencher, barchart: &BarChart) {
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
// We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched(
|| barchart.clone(),
|bench_barchart| {
bench_barchart.render(buffer.area, &mut buffer);
},
criterion::BatchSize::LargeInput,
)
}
criterion_group!(benches, barchart);
criterion_main!(benches);

View File

@@ -21,8 +21,8 @@ body = """
{% endif -%}
{% macro commit(commit) -%}
- *({{commit.scope | default(value = "uncategorized")}})* {{ commit.message | upper_first }}
([{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui-org/ratatui/commit/" ~ commit.id }}))
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui-org/ratatui/commit/" ~ commit.id }})
*({{commit.scope | default(value = "uncategorized") | lower }})* {{ commit.message | upper_first }}
{%- if commit.breaking %} [**breaking**]{% endif %}
{%- if commit.body %}
@@ -80,6 +80,8 @@ commit_parsers = [
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore\\(deps\\)", skip = true },
{ message = "^chore\\(changelog\\)", skip = true },
{ message = "^[cC]hore", group = "<!-- 07 -->Miscellaneous Tasks" },
{ body = ".*security", group = "<!-- 08 -->Security" },
{ message = "^build", group = "<!-- 09 -->Build" },

View File

@@ -1,2 +1,14 @@
coverage: # https://docs.codecov.com/docs/codecovyml-reference#coverage
precision: 1 # e.g. 89.1%
round: down
range: 85..100 # https://docs.codecov.com/docs/coverage-configuration#section-range
status: # https://docs.codecov.com/docs/commit-status
project:
default:
threshold: 1% # Avoid false negatives
ignore:
- "examples"
- "benches"
comment: # https://docs.codecov.com/docs/pull-request-comments
# make the comments less noisy
require_changes: true

View File

@@ -1,14 +1,23 @@
# Examples
These gifs were created using [Charm VHS](https://github.com/charmbracelet/vhs).
These gifs were created using [VHS](https://github.com/charmbracelet/vhs). Each example has a
corresponding `.tape` file that holds instructions for how to generate the images. Note that the
images themselves are stored in a separate git branch to avoid bloating the main repository.
VHS has a problem rendering some background color transitions, which shows up in several examples
below. See <https://github.com/charmbracelet/vhs/issues/344> for more info. These problems don't
occur in a terminal.
## Demo2
## Demo ([demo.rs](./demo/))
This is the demo example from the main README and crate page. Source: [demo2](./demo2/).
This is the demo example from the main README. It is available for each of the backends.
```shell
cargo run --example=demo2 --features="crossterm widget-calendar"
```
![Demo2][demo2.gif]
## Demo
This is the previous demo example from the main README. It is available for each of the backends. Source:
[demo.rs](./demo/).
```shell
cargo run --example=demo --features=crossterm
@@ -18,7 +27,21 @@ cargo run --example=demo --no-default-features --features=termwiz
![Demo][demo.gif]
## Barchart ([barchart.rs](./barchart.rs)
## Hello World
This is a pretty boring example, but it contains some good documentation
on writing tui apps. Source: [hello_world.rs](./hello_world.rs).
```shell
cargo run --example=hello_world --features=crossterm
```
![Hello World][hello_world.gif]
## Barchart
Demonstrates the [`BarChart`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
widget. Source: [barchart.rs](./barchart.rs).
```shell
cargo run --example=barchart --features=crossterm
@@ -26,7 +49,10 @@ cargo run --example=barchart --features=crossterm
![Barchart][barchart.gif]
## Block ([block.rs](./block.rs))
## Block
Demonstrates the [`Block`](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html)
widget. Source: [block.rs](./block.rs).
```shell
cargo run --example=block --features=crossterm
@@ -34,15 +60,23 @@ cargo run --example=block --features=crossterm
![Block][block.gif]
## Calendar ([calendar.rs](./calendar.rs))
## Calendar
Demonstrates the [`Calendar`](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
widget. Source: [calendar.rs](./calendar.rs).
```shell
cargo run --example=calendar --features=crossterm widget-calendar
cargo run --example=calendar --features="crossterm widget-calendar"
```
![Calendar][calendar.gif]
## Canvas ([canvas.rs](./canvas.rs))
## Canvas
Demonstrates the [`Canvas`](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html) widget
and related shapes in the
[`canvas`](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html) module. Source:
[canvas.rs](./canvas.rs).
```shell
cargo run --example=canvas --features=crossterm
@@ -50,7 +84,10 @@ cargo run --example=canvas --features=crossterm
![Canvas][canvas.gif]
## Chart ([chart.rs](./chart.rs))
## Chart
Demonstrates the [`Chart`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Chart.html) widget.
Source: [chart.rs](./chart.rs).
```shell
cargo run --example=chart --features=crossterm
@@ -58,7 +95,10 @@ cargo run --example=chart --features=crossterm
![Chart][chart.gif]
## Colors ([colors.rs](./colors.rs))
## Colors
Demonstrates the available [`Color`](https://docs.rs/ratatui/latest/ratatui/style/enum.Color.html)
options. These can be used in any style field. Source: [colors.rs](./colors.rs).
```shell
cargo run --example=colors --features=crossterm
@@ -66,21 +106,38 @@ cargo run --example=colors --features=crossterm
![Colors][colors.gif]
## Custom Widget ([custom_widget.rs](./custom_widget.rs))
## Colors (RGB)
Demonstrates the available RGB
[`Color`](https://docs.rs/ratatui/latest/ratatui/style/enum.Color.html) options. These can be used
in any style field. Source: [colors_rgb.rs](./colors_rgb.rs). Uses a half block technique to render
two square-ish pixels in the space of a single rectangular terminal cell.
```shell
cargo run --example=colors_rgb --features=crossterm
```
Note: VHs renders full screen animations poorly, so this is a screen capture rather than the output
of the VHS tape.
<https://github.com/ratatui-org/ratatui/assets/381361/485e775a-e0b5-4133-899b-1e8aeb56e774>
## Custom Widget
Demonstrates how to implement the
[`Widget`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.Widget.html) trait. Also shows mouse
interaction. Source: [custom_widget.rs](./custom_widget.rs).
```shell
cargo run --example=custom_widget --features=crossterm
```
This is not a particularly exciting example visually, but it demonstrates how to implement your own widget.
![Custom Widget][custom_widget.gif]
## Gauge ([gauge.rs](./gauge.rs))
## Gauge
Please note: the background renders poorly when we generate this example using VHS.
This problem doesn't generally happen during normal rendering in a terminal.
See <https://github.com/charmbracelet/vhs/issues/344> for more details
Demonstrates the [`Gauge`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Gauge.html) widget.
Source: [gauge.rs](./gauge.rs).
```shell
cargo run --example=gauge --features=crossterm
@@ -88,18 +145,11 @@ cargo run --example=gauge --features=crossterm
![Gauge][gauge.gif]
## Hello World ([hello_world.rs](./hello_world.rs))
## Inline
```shell
cargo run --example=hello_world --features=crossterm
```
This is a pretty boring example, but it contains some good comments of documentation on some of the
standard approaches to writing tui apps.
![Hello World][hello_world.gif]
## Inline ([inline.rs](./inline.rs))
Demonstrates how to use the
[`Inline`](https://docs.rs/ratatui/latest/ratatui/terminal/enum.Viewport.html#variant.Inline)
Viewport mode for ratatui apps. Source: [inline.rs](./inline.rs).
```shell
cargo run --example=inline --features=crossterm
@@ -107,7 +157,10 @@ cargo run --example=inline --features=crossterm
![Inline][inline.gif]
## Layout ([layout.rs](./layout.rs))
## Layout
Demonstrates the [`Layout`](https://docs.rs/ratatui/latest/ratatui/layout/struct.Layout.html) and
interaction between each constraint. Source: [layout.rs](./layout.rs).
```shell
cargo run --example=layout --features=crossterm
@@ -115,7 +168,10 @@ cargo run --example=layout --features=crossterm
![Layout][layout.gif]
## List ([list.rs](./list.rs))
## List
Demonstrates the [`List`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html) widget.
Source: [list.rs](./list.rs).
```shell
cargo run --example=list --features=crossterm
@@ -123,7 +179,11 @@ cargo run --example=list --features=crossterm
![List][list.gif]
## Modifiers ([modifiers.rs](./modifiers.rs))
## Modifiers
Demonstrates the style
[`Modifiers`](https://docs.rs/ratatui/latest/ratatui/style/struct.Modifier.html). Source:
[modifiers.rs](./modifiers.rs).
```shell
cargo run --example=modifiers --features=crossterm
@@ -131,7 +191,10 @@ cargo run --example=modifiers --features=crossterm
![Modifiers][modifiers.gif]
## Panic ([panic.rs](./panic.rs))
## Panic
Demonstrates how to handle panics by ensuring that panic messages are written correctly to the
screen. Source: [panic.rs](./panic.rs).
```shell
cargo run --example=panic --features=crossterm
@@ -139,7 +202,10 @@ cargo run --example=panic --features=crossterm
![Panic][panic.gif]
## Paragraph ([paragraph.rs](./paragraph.rs))
## Paragraph
Demonstrates the [`Paragraph`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Paragraph.html)
widget. Source: [paragraph.rs](./paragraph.rs)
```shell
cargo run --example=paragraph --features=crossterm
@@ -147,19 +213,35 @@ cargo run --example=paragraph --features=crossterm
![Paragraph][paragraph.gif]
## Popup ([popup.rs](./popup.rs))
## Popup
Demonstrates how to render a widget over the top of previously rendered widgets using the
[`Clear`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Clear.html) widget. Source:
[popup.rs](./popup.rs).
>
```shell
cargo run --example=popup --features=crossterm
```
Please note: the background renders poorly when we generate this example using VHS.
This problem doesn't generally happen during normal rendering in a terminal.
See <https://github.com/charmbracelet/vhs/issues/344> for more details
![Popup][popup.gif]
## Scrollbar ([scrollbar.rs](./scrollbar.rs))
## Ratatui-logo
A fun example of using half blocks to render graphics Source:
[ratatui-logo.rs](./ratatui-logo.rs).
>
```shell
cargo run --example=ratatui-logo --features=crossterm
```
![Ratatui Logo][ratatui-logo.gif]
## Scrollbar
Demonstrates the [`Scrollbar`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Scrollbar.html)
widget. Source: [scrollbar.rs](./scrollbar.rs).
```shell
cargo run --example=scrollbar --features=crossterm
@@ -167,7 +249,10 @@ cargo run --example=scrollbar --features=crossterm
![Scrollbar][scrollbar.gif]
## Sparkline ([sparkline.rs](./sparkline.rs))
## Sparkline
Demonstrates the [`Sparkline`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Sparkline.html)
widget. Source: [sparkline.rs](./sparkline.rs).
```shell
cargo run --example=sparkline --features=crossterm
@@ -175,7 +260,10 @@ cargo run --example=sparkline --features=crossterm
![Sparkline][sparkline.gif]
## Table ([table.rs](./table.rs))
## Table
Demonstrates the [`Table`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Table.html) widget.
Source: [table.rs](./table.rs).
```shell
cargo run --example=table --features=crossterm
@@ -183,7 +271,10 @@ cargo run --example=table --features=crossterm
![Table][table.gif]
## Tabs ([tabs.rs](./tabs.rs))
## Tabs
Demonstrates the [`Tabs`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Tabs.html) widget.
Source: [tabs.rs](./tabs.rs).
```shell
cargo run --example=tabs --features=crossterm
@@ -191,7 +282,13 @@ cargo run --example=tabs --features=crossterm
![Tabs][tabs.gif]
## User Input ([user_input.rs](./user_input.rs))
## User Input
Demonstrates one approach to accepting user input. Source [user_input.rs](./user_input.rs).
> [!NOTE]
> Consider using [`tui-textarea`](https://crates.io/crates/tui-textarea) or
> [`tui-input`](https://crates.io/crates/tui-input) crates for more functional text entry UIs.
```shell
cargo run --example=user_input --features=crossterm
@@ -205,36 +302,31 @@ These are generated with `vhs publish examples/xxx.gif`
To update these examples in bulk:
```shell
# build to ensure that running the examples doesn't have to wait so long
cargo build --examples --features=crossterm,all-widgets
for i in examples/*.tape
do
echo -n "[${i:s:examples/:::s:.tape:.gif:}]: "
vhs $i --publish --quiet
# may need to adjust this depending on if you see rate limiting from VHS
sleep 1
done
examples/generate.bash
```
-->
[barchart.gif]: https://vhs.charm.sh/vhs-6ioxdeRBVkVpyXcjIEVaJU.gif
[block.gif]: https://vhs.charm.sh/vhs-1TyeDa5GN7kewhNjKxJ4Br.gif
[calendar.gif]: https://vhs.charm.sh/vhs-1dBcpMSSP80WkBgm4lBhNo.gif
[canvas.gif]: https://vhs.charm.sh/vhs-4zeWEPF6bLEFSHuJrvaHlN.gif
[chart.gif]: https://vhs.charm.sh/vhs-zRzsE2AwRixQhcWMTAeF1.gif
[colors.gif]: https://vhs.charm.sh/vhs-2ZCqYbTbXAaASncUeWkt1z.gif
[custom_widget.gif]: https://vhs.charm.sh/vhs-32mW1TpkrovTcm79QXmBSu.gif
[demo.gif]: https://vhs.charm.sh/vhs-tF0QbuPbtHgUeG0sTVgFr.gif
[gauge.gif]: https://vhs.charm.sh/vhs-2rvSeP5r4lRkGTzNCKpm9a.gif
[hello_world.gif]: https://vhs.charm.sh/vhs-3CKUwxFuQi8oKQMS5zkPfQ.gif
[inline.gif]: https://vhs.charm.sh/vhs-miRl1mosKFoJV7LjjvF4T.gif
[layout.gif]: https://vhs.charm.sh/vhs-1ZNoNLNlLtkJXpgg9nCV5e.gif
[list.gif]: https://vhs.charm.sh/vhs-4goo9reeUM9r0nYb54R7SP.gif
[modifiers.gif]: https://vhs.charm.sh/vhs-2ovGBz5l3tfRGdZ7FCw0am.gif
[panic.gif]: https://vhs.charm.sh/vhs-HrvKCHV4yeN69fb1EadTH.gif
[paragraph.gif]: https://vhs.charm.sh/vhs-2qIPDi79DUmtmeNDEeHVEF.gif
[popup.gif]: https://vhs.charm.sh/vhs-2QnC682AUeNYNXcjNlKTyp.gif
[scrollbar.gif]: https://vhs.charm.sh/vhs-2p13MMFreW7Gwt1xIonIWu.gif
[sparkline.gif]: https://vhs.charm.sh/vhs-4t59Vxw5Za33Rtvt9QrftA.gif
[table.gif]: https://vhs.charm.sh/vhs-6IrGHgT385DqA6xnwGF9oD.gif
[tabs.gif]: https://vhs.charm.sh/vhs-61WkbfhyDk0kbkjncErdHT.gif
[user_input.gif]: https://vhs.charm.sh/vhs-4fxUgkpEWcVyBRXuyYKODY.gif
[barchart.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/barchart.gif?raw=true
[block.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/block.gif?raw=true
[calendar.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/calendar.gif?raw=true
[canvas.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/canvas.gif?raw=true
[chart.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/chart.gif?raw=true
[colors.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/colors.gif?raw=true
[custom_widget.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/custom_widget.gif?raw=true
[demo.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/demo.gif?raw=true
[demo2.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/demo2.gif?raw=true
[gauge.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/gauge.gif?raw=true
[hello_world.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/hello_world.gif?raw=true
[inline.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/inline.gif?raw=true
[layout.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/layout.gif?raw=true
[list.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/list.gif?raw=true
[modifiers.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/modifiers.gif?raw=true
[panic.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/panic.gif?raw=true
[paragraph.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/paragraph.gif?raw=true
[popup.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/popup.gif?raw=true
[ratatui-logo.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/ratatui-logo.gif?raw=true
[scrollbar.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/scrollbar.gif?raw=true
[sparkline.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/sparkline.gif?raw=true
[table.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/table.gif?raw=true
[tabs.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/tabs.gif?raw=true
[user_input.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/user_input.gif?raw=true

View File

@@ -119,9 +119,7 @@ fn run_app<B: Backend>(
loop {
terminal.draw(|f| ui(f, &app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
@@ -136,11 +134,10 @@ fn run_app<B: Backend>(
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
fn ui(f: &mut Frame, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)].as_ref())
.constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)])
.split(f.size());
let barchart = BarChart::default()
@@ -153,7 +150,7 @@ 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())
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[1]);
draw_bar_with_group_labels(f, app, chunks[0]);
@@ -199,10 +196,7 @@ fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGr
.collect()
}
fn draw_bar_with_group_labels<B>(f: &mut Frame<B>, app: &App, area: Rect)
where
B: Backend,
{
fn draw_bar_with_group_labels(f: &mut Frame, app: &App, area: Rect) {
let groups = create_groups(app, false);
let mut barchart = BarChart::default()
@@ -229,10 +223,7 @@ where
}
}
fn draw_horizontal_bars<B>(f: &mut Frame<B>, app: &App, area: Rect)
where
B: Backend,
{
fn draw_horizontal_bars(f: &mut Frame, app: &App, area: Rect) {
let groups = create_groups(app, true);
let mut barchart = BarChart::default()
@@ -261,10 +252,7 @@ where
}
}
fn draw_legend<B>(f: &mut Frame<B>, area: Rect)
where
B: Backend,
{
fn draw_legend(f: &mut Frame, area: Rect) {
let text = vec![
Line::from(Span::styled(
TOTAL_REVENUE,

View File

@@ -1,6 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/barchart.tape`
Output "target/barchart.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 800
Hide

View File

@@ -21,7 +21,6 @@ use ratatui::{
// These type aliases are used to make the code more readable by reducing repetition of the generic
// types. They are not necessary for the functionality of the code.
type Frame<'a> = ratatui::Frame<'a, CrosstermBackend<Stdout>>;
type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
type Result<T> = std::result::Result<T, Box<dyn Error>>;
@@ -106,18 +105,18 @@ fn ui(frame: &mut Frame) {
fn calculate_layout(area: Rect) -> (Rect, Vec<Vec<Rect>>) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1), Constraint::Min(0)])
.constraints([Constraint::Length(1), Constraint::Min(0)])
.split(area);
let title_area = layout[0];
let main_areas = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Max(4); 9])
.constraints([Constraint::Max(4); 9])
.split(layout[1])
.iter()
.map(|&area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(area)
.to_vec()
})

View File

@@ -1,7 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/block.tape`
Output "target/block.gif"
Set Theme "Builtin Dark"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 1200
Hide

View File

@@ -16,7 +16,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut terminal = Terminal::new(backend)?;
loop {
let _ = terminal.draw(|f| draw(f));
let _ = terminal.draw(draw);
if let Event::Key(key) = event::read()? {
#[allow(clippy::single_match)]
@@ -35,7 +35,7 @@ fn main() -> Result<(), Box<dyn Error>> {
Ok(())
}
fn draw<B: Backend>(f: &mut Frame<B>) {
fn draw(f: &mut Frame) {
let app_area = f.size();
let calarea = Rect {
@@ -69,14 +69,11 @@ 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(),
);
.constraints([
Constraint::Percentage(33),
Constraint::Percentage(33),
Constraint::Percentage(33),
]);
list_layout.split(*area)
}
@@ -85,15 +82,12 @@ 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(),
);
.constraints([
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
]);
list_layout.split(*area)
}
@@ -181,7 +175,7 @@ fn make_dates(current_year: i32) -> CalendarEventStore {
mod cals {
use super::*;
pub(super) fn get_cal<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
pub(super) fn get_cal<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
use Month::*;
match m {
May => example1(m, y, es),
@@ -194,7 +188,7 @@ mod cals {
}
}
fn default<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
fn default<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
let default_style = Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Rgb(50, 50, 50));
@@ -204,7 +198,7 @@ mod cals {
.default_style(default_style)
}
fn example1<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
fn example1<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
let default_style = Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Rgb(50, 50, 50));
@@ -215,7 +209,7 @@ mod cals {
.show_month_header(Style::default())
}
fn example2<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
fn example2<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
let header_style = Style::default()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::DIM)
@@ -231,7 +225,7 @@ mod cals {
.show_month_header(Style::default())
}
fn example3<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
fn example3<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
let header_style = Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Green);
@@ -247,7 +241,7 @@ mod cals {
.show_month_header(Style::default())
}
fn example4<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
fn example4<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
let header_style = Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Green);
@@ -261,7 +255,7 @@ mod cals {
.default_style(default_style)
}
fn example5<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
fn example5<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
let header_style = Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Green);

View File

@@ -1,6 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/calendar.tape`
Output "target/calendar.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 800
Hide

View File

@@ -1,28 +1,29 @@
use std::{
error::Error,
io,
io::{self, stdout, Stdout},
time::{Duration, Instant},
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{
prelude::*,
widgets::{canvas::*, *},
};
fn main() -> io::Result<()> {
App::run()
}
struct App {
x: f64,
y: f64,
ball: Rectangle,
ball: Circle,
playground: Rect,
vx: f64,
vy: f64,
dir_x: bool,
dir_y: bool,
tick_count: u64,
marker: Marker,
}
@@ -32,155 +33,166 @@ impl App {
App {
x: 0.0,
y: 0.0,
ball: Rectangle {
x: 10.0,
y: 30.0,
width: 10.0,
height: 10.0,
ball: Circle {
x: 20.0,
y: 40.0,
radius: 10.0,
color: Color::Yellow,
},
playground: Rect::new(10, 10, 100, 100),
playground: Rect::new(10, 10, 200, 100),
vx: 1.0,
vy: 1.0,
dir_x: true,
dir_y: true,
tick_count: 0,
marker: Marker::Dot,
}
}
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
{
self.dir_x = !self.dir_x;
}
if self.ball.y < self.playground.top() as f64
|| self.ball.y + self.ball.height > self.playground.bottom() as f64
{
self.dir_y = !self.dir_y;
}
if self.dir_x {
self.ball.x += self.vx;
} else {
self.ball.x -= self.vx;
}
if self.dir_y {
self.ball.y += self.vy;
} else {
self.ball.y -= self.vy
}
}
}
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 tick_rate = Duration::from_millis(250);
let app = App::new();
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(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(());
pub fn run() -> io::Result<()> {
let mut terminal = init_terminal()?;
let mut app = App::new();
let mut last_tick = Instant::now();
let tick_rate = Duration::from_millis(16);
loop {
let _ = terminal.draw(|frame| app.ui(frame));
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => break,
KeyCode::Down | KeyCode::Char('j') => app.y += 1.0,
KeyCode::Up | KeyCode::Char('k') => app.y -= 1.0,
KeyCode::Right | KeyCode::Char('l') => app.x += 1.0,
KeyCode::Left | KeyCode::Char('h') => app.x -= 1.0,
_ => {}
}
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();
}
}
restore_terminal()
}
fn on_tick(&mut self) {
self.tick_count += 1;
// only change marker every 180 ticks (3s) to avoid stroboscopic effect
if (self.tick_count % 180) == 0 {
self.marker = match self.marker {
Marker::Dot => Marker::Braille,
Marker::Braille => Marker::Block,
Marker::Block => Marker::HalfBlock,
Marker::HalfBlock => Marker::Bar,
Marker::Bar => Marker::Dot,
};
}
// bounce the ball by flipping the velocity vector
let ball = &self.ball;
let playground = self.playground;
if ball.x - ball.radius < playground.left() as f64
|| ball.x + ball.radius > playground.right() as f64
{
self.vx = -self.vx;
}
if ball.y - ball.radius < playground.top() as f64
|| ball.y + ball.radius > playground.bottom() as f64
{
self.vy = -self.vy;
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
self.ball.x += self.vx;
self.ball.y += self.vy;
}
fn ui(&self, frame: &mut Frame) {
let main_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(frame.size());
let right_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(main_layout[1]);
frame.render_widget(self.map_canvas(), main_layout[0]);
frame.render_widget(self.pong_canvas(), right_layout[0]);
frame.render_widget(self.boxes_canvas(right_layout[1]), right_layout[1]);
}
fn map_canvas(&self) -> impl Widget + '_ {
Canvas::default()
.block(Block::default().borders(Borders::ALL).title("World"))
.marker(self.marker)
.paint(|ctx| {
ctx.draw(&Map {
color: Color::Green,
resolution: MapResolution::High,
});
ctx.print(self.x, -self.y, "You are here".yellow());
})
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0])
}
fn pong_canvas(&self) -> impl Widget + '_ {
Canvas::default()
.block(Block::default().borders(Borders::ALL).title("Pong"))
.marker(self.marker)
.paint(|ctx| {
ctx.draw(&self.ball);
})
.x_bounds([10.0, 210.0])
.y_bounds([10.0, 110.0])
}
fn boxes_canvas(&self, area: Rect) -> impl Widget {
let (left, right, bottom, top) =
(0.0, area.width as f64, 0.0, area.height as f64 * 2.0 - 4.0);
Canvas::default()
.block(Block::default().borders(Borders::ALL).title("Rects"))
.marker(self.marker)
.x_bounds([left, right])
.y_bounds([bottom, top])
.paint(|ctx| {
for i in 0..=11 {
ctx.draw(&Rectangle {
x: (i * i + 3 * i) as f64 / 2.0 + 2.0,
y: 2.0,
width: i as f64,
height: i as f64,
color: Color::Red,
});
ctx.draw(&Rectangle {
x: (i * i + 3 * i) as f64 / 2.0 + 2.0,
y: 21.0,
width: i as f64,
height: i as f64,
color: Color::Blue,
});
}
for i in 0..100 {
if i % 10 != 0 {
ctx.print(i as f64 + 1.0, 0.0, format!("{i}", i = i % 10));
}
if i % 2 == 0 && i % 10 != 0 {
ctx.print(0.0, i as f64, format!("{i}", i = i % 10));
}
}
})
}
}
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, "You are here".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]);
fn init_terminal() -> io::Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
Terminal::new(CrosstermBackend::new(stdout()))
}
fn restore_terminal() -> io::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -1,11 +1,13 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/canvas.tape`
Output "target/canvas.gif"
Set Theme "Aardvark Blue"
Set FontSize 12
Set Width 1200
Set Height 800
Hide
Type "cargo run --example=canvas --features=crossterm"
Enter
Sleep 1s
Sleep 2s
Show
Sleep 5s
Sleep 30s

View File

@@ -9,18 +9,10 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
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)];
const DATA2: [(f64, f64); 7] = [
(0.0, 0.0),
(10.0, 1.0),
(20.0, 0.5),
(30.0, 1.5),
(40.0, 1.0),
(50.0, 2.5),
(60.0, 3.0),
];
use ratatui::{
prelude::*,
widgets::{block::Title, *},
};
#[derive(Clone)]
pub struct SinSignal {
@@ -125,9 +117,7 @@ fn run_app<B: Backend>(
loop {
terminal.draw(|f| ui(f, &app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
@@ -142,19 +132,30 @@ fn run_app<B: Backend>(
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
fn ui(f: &mut Frame, 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 vertical_chunks = Layout::new(
Direction::Vertical,
[Constraint::Percentage(40), Constraint::Percentage(60)],
)
.split(size);
// top chart
render_chart1(f, vertical_chunks[0], app);
let horizontal_chunks = Layout::new(
Direction::Horizontal,
[Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
)
.split(vertical_chunks[1]);
// bottom left
render_line_chart(f, horizontal_chunks[0]);
// bottom right
render_scatter(f, horizontal_chunks[1]);
}
fn render_chart1(f: &mut Frame, area: Rect, app: &App) {
let x_labels = vec![
Span::styled(
format!("{}", app.window[0]),
@@ -199,61 +200,164 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
.labels(vec!["-20".bold(), "0".into(), "20".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("Chart 2".cyan().bold())
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.bounds([0.0, 5.0])
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.bounds([0.0, 5.0])
.labels(vec!["0".bold(), "2.5".into(), "5.0".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("Chart 3".cyan().bold())
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.bounds([0.0, 50.0])
.labels(vec!["0".bold(), "25".into(), "50".bold()]),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.bounds([0.0, 5.0])
.labels(vec!["0".bold(), "2.5".into(), "5".bold()]),
);
f.render_widget(chart, chunks[2]);
f.render_widget(chart, area);
}
fn render_line_chart(f: &mut Frame, area: Rect) {
let datasets = vec![Dataset::default()
.name("Line from only 2 points")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.graph_type(GraphType::Line)
.data(&[(1., 1.), (4., 4.)])];
let chart = Chart::new(datasets)
.block(
Block::default()
.title(
Title::default()
.content("Line chart".cyan().bold())
.alignment(Alignment::Center),
)
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().gray())
.bounds([0.0, 5.0])
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().gray())
.bounds([0.0, 5.0])
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
)
.legend_position(Some(LegendPosition::TopLeft))
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
f.render_widget(chart, area)
}
fn render_scatter(f: &mut Frame, area: Rect) {
let datasets = vec![
Dataset::default()
.name("Heavy")
.marker(Marker::Dot)
.graph_type(GraphType::Scatter)
.style(Style::new().yellow())
.data(&HEAVY_PAYLOAD_DATA),
Dataset::default()
.name("Medium")
.marker(Marker::Braille)
.graph_type(GraphType::Scatter)
.style(Style::new().magenta())
.data(&MEDIUM_PAYLOAD_DATA),
Dataset::default()
.name("Small")
.marker(Marker::Dot)
.graph_type(GraphType::Scatter)
.style(Style::new().cyan())
.data(&SMALL_PAYLOAD_DATA),
];
let chart = Chart::new(datasets)
.block(
Block::new().borders(Borders::all()).title(
Title::default()
.content("Scatter chart".cyan().bold())
.alignment(Alignment::Center),
),
)
.x_axis(
Axis::default()
.title("Year")
.bounds([1960., 2020.])
.style(Style::default().fg(Color::Gray))
.labels(vec!["1960".into(), "1990".into(), "2020".into()]),
)
.y_axis(
Axis::default()
.title("Cost")
.bounds([0., 75000.])
.style(Style::default().fg(Color::Gray))
.labels(vec!["0".into(), "37 500".into(), "75 000".into()]),
)
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
f.render_widget(chart, area);
}
// Data from https://ourworldindata.org/space-exploration-satellites
const HEAVY_PAYLOAD_DATA: [(f64, f64); 9] = [
(1965., 8200.),
(1967., 5400.),
(1981., 65400.),
(1989., 30800.),
(1997., 10200.),
(2004., 11600.),
(2014., 4500.),
(2016., 7900.),
(2018., 1500.),
];
const MEDIUM_PAYLOAD_DATA: [(f64, f64); 29] = [
(1963., 29500.),
(1964., 30600.),
(1965., 177900.),
(1965., 21000.),
(1966., 17900.),
(1966., 8400.),
(1975., 17500.),
(1982., 8300.),
(1985., 5100.),
(1988., 18300.),
(1990., 38800.),
(1990., 9900.),
(1991., 18700.),
(1992., 9100.),
(1994., 10500.),
(1994., 8500.),
(1994., 8700.),
(1997., 6200.),
(1999., 18000.),
(1999., 7600.),
(1999., 8900.),
(1999., 9600.),
(2000., 16000.),
(2001., 10000.),
(2002., 10400.),
(2002., 8100.),
(2010., 2600.),
(2013., 13600.),
(2017., 8000.),
];
const SMALL_PAYLOAD_DATA: [(f64, f64); 23] = [
(1961., 118500.),
(1962., 14900.),
(1975., 21400.),
(1980., 32800.),
(1988., 31100.),
(1990., 41100.),
(1993., 23600.),
(1994., 20600.),
(1994., 34600.),
(1996., 50600.),
(1997., 19200.),
(1997., 45800.),
(1998., 19100.),
(2000., 73100.),
(2003., 11200.),
(2008., 12600.),
(2010., 30500.),
(2012., 20000.),
(2013., 10600.),
(2013., 34500.),
(2015., 10600.),
(2018., 23100.),
(2019., 17300.),
];

View File

@@ -1,6 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/chart.tape`
Output "target/chart.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 800
Hide

View File

@@ -41,10 +41,10 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
}
}
fn ui<B: Backend>(frame: &mut Frame<B>) {
fn ui(frame: &mut Frame) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
.constraints([
Constraint::Length(30),
Constraint::Length(17),
Constraint::Length(2),
@@ -75,10 +75,10 @@ const NAMED_COLORS: [Color; 16] = [
Color::White,
];
fn render_named_colors<B: Backend>(frame: &mut Frame<B>, area: Rect) {
fn render_named_colors(frame: &mut Frame, area: Rect) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(3); 10])
.constraints([Constraint::Length(3); 10])
.split(area);
render_fg_named_colors(frame, Color::Reset, layout[0]);
@@ -94,20 +94,20 @@ fn render_named_colors<B: Backend>(frame: &mut Frame<B>, area: Rect) {
render_bg_named_colors(frame, Color::White, layout[9]);
}
fn render_fg_named_colors<B: Backend>(frame: &mut Frame<B>, bg: Color, area: Rect) {
fn render_fg_named_colors(frame: &mut Frame, bg: Color, area: Rect) {
let block = title_block(format!("Foreground colors on {bg} background"));
let inner = block.inner(area);
frame.render_widget(block, area);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1); 2])
.constraints([Constraint::Length(1); 2])
.split(inner)
.iter()
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Ratio(1, 8); 8])
.constraints([Constraint::Ratio(1, 8); 8])
.split(*area)
.to_vec()
})
@@ -119,20 +119,20 @@ fn render_fg_named_colors<B: Backend>(frame: &mut Frame<B>, bg: Color, area: Rec
}
}
fn render_bg_named_colors<B: Backend>(frame: &mut Frame<B>, fg: Color, area: Rect) {
fn render_bg_named_colors(frame: &mut Frame, fg: Color, area: Rect) {
let block = title_block(format!("Background colors with {fg} foreground"));
let inner = block.inner(area);
frame.render_widget(block, area);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1); 2])
.constraints([Constraint::Length(1); 2])
.split(inner)
.iter()
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Ratio(1, 8); 8])
.constraints([Constraint::Ratio(1, 8); 8])
.split(*area)
.to_vec()
})
@@ -144,14 +144,14 @@ fn render_bg_named_colors<B: Backend>(frame: &mut Frame<B>, fg: Color, area: Rec
}
}
fn render_indexed_colors<B: Backend>(frame: &mut Frame<B>, area: Rect) {
fn render_indexed_colors(frame: &mut Frame, area: Rect) {
let block = title_block("Indexed colors".into());
let inner = block.inner(area);
frame.render_widget(block, area);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
.constraints([
Constraint::Length(1), // 0 - 15
Constraint::Length(1), // blank
Constraint::Min(6), // 16 - 123
@@ -164,7 +164,7 @@ fn render_indexed_colors<B: Backend>(frame: &mut Frame<B>, area: Rect) {
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
let color_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Length(5); 16])
.constraints([Constraint::Length(5); 16])
.split(layout[0]);
for i in 0..16 {
let color = Color::Indexed(i);
@@ -198,7 +198,7 @@ fn render_indexed_colors<B: Backend>(frame: &mut Frame<B>, area: Rect) {
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Length(27); 3])
.constraints([Constraint::Length(27); 3])
.split(*area)
.to_vec()
})
@@ -206,7 +206,7 @@ fn render_indexed_colors<B: Backend>(frame: &mut Frame<B>, area: Rect) {
.flat_map(|area| {
Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1); 6])
.constraints([Constraint::Length(1); 6])
.split(area)
.to_vec()
})
@@ -214,7 +214,7 @@ fn render_indexed_colors<B: Backend>(frame: &mut Frame<B>, area: Rect) {
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Min(4); 6])
.constraints([Constraint::Min(4); 6])
.split(area)
.to_vec()
})
@@ -243,10 +243,10 @@ fn title_block(title: String) -> Block<'static> {
.title_style(Style::new().reset())
}
fn render_indexed_grayscale<B: Backend>(frame: &mut Frame<B>, area: Rect) {
fn render_indexed_grayscale(frame: &mut Frame, area: Rect) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
.constraints([
Constraint::Length(1), // 232 - 243
Constraint::Length(1), // 244 - 255
])
@@ -255,7 +255,7 @@ fn render_indexed_grayscale<B: Backend>(frame: &mut Frame<B>, area: Rect) {
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Length(6); 12])
.constraints([Constraint::Length(6); 12])
.split(*area)
.to_vec()
})

View File

@@ -1,13 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/colors.tape`
Output "target/colors.gif"
# The OceanicMaterial theme is a good choice for this example (Obsidian is almost as good) because:
# - Black is dark and distinct from the default background
# - White is light and distinct from the default foreground
# - Normal and bright colors are distinct
# - Black and DarkGray are distinct
# - White and Gray are distinct
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 1410
Hide

229
examples/colors_rgb.rs Normal file
View File

@@ -0,0 +1,229 @@
/// This example shows the full range of RGB colors that can be displayed in the terminal.
///
/// Requires a terminal that supports 24-bit color (true color) and unicode.
use std::{
io::stdout,
time::{Duration, Instant},
};
use color_eyre::config::HookBuilder;
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use palette::{convert::FromColorUnclamped, Okhsv, Srgb};
use ratatui::{prelude::*, widgets::*};
fn main() -> color_eyre::Result<()> {
App::run()
}
#[derive(Debug, Default)]
struct App {
should_quit: bool,
// a 2d vec of the colors to render, calculated when the size changes as this is expensive
// to calculate every frame
colors: Vec<Vec<Color>>,
last_size: Rect,
fps: Fps,
frame_count: usize,
}
#[derive(Debug)]
struct Fps {
frame_count: usize,
last_instant: Instant,
fps: Option<f32>,
}
struct AppWidget<'a> {
title: Paragraph<'a>,
fps_widget: FpsWidget<'a>,
rgb_colors_widget: RgbColorsWidget<'a>,
}
struct FpsWidget<'a> {
fps: &'a Fps,
}
struct RgbColorsWidget<'a> {
/// The colors to render - should be double the height of the area
colors: &'a Vec<Vec<Color>>,
/// the number of elapsed frames that have passed - used to animate the colors
frame_count: usize,
}
impl App {
pub fn run() -> color_eyre::Result<()> {
install_panic_hook()?;
let mut terminal = init_terminal()?;
let mut app = Self::default();
while !app.should_quit {
app.tick();
terminal.draw(|frame| {
let size = frame.size();
app.setup_colors(size);
frame.render_widget(AppWidget::new(&app), size);
})?;
app.handle_events()?;
}
restore_terminal()?;
Ok(())
}
fn tick(&mut self) {
self.frame_count += 1;
self.fps.tick();
}
fn handle_events(&mut self) -> color_eyre::Result<()> {
if event::poll(Duration::from_secs_f32(1.0 / 60.0))? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
self.should_quit = true;
};
}
}
Ok(())
}
fn setup_colors(&mut self, size: Rect) {
// only update the colors if the size has changed since the last time we rendered
if self.last_size.width == size.width && self.last_size.height == size.height {
return;
}
self.last_size = size;
let Rect { width, height, .. } = size;
// double the height because each screen row has two rows of half block pixels
let height = height * 2;
self.colors.clear();
for y in 0..height {
let mut row = Vec::new();
for x in 0..width {
let hue = x as f32 * 360.0 / width as f32;
let value = (height - y) as f32 / height as f32;
let saturation = Okhsv::max_saturation();
let color = Okhsv::new(hue, saturation, value);
let color = Srgb::<f32>::from_color_unclamped(color);
let color: Srgb<u8> = color.into_format();
let color = Color::Rgb(color.red, color.green, color.blue);
row.push(color);
}
self.colors.push(row);
}
}
}
impl Fps {
fn tick(&mut self) {
self.frame_count += 1;
let elapsed = self.last_instant.elapsed();
// update the fps every second, but only if we've rendered at least 2 frames (to avoid
// noise in the fps calculation)
if elapsed > Duration::from_secs(1) && self.frame_count > 2 {
self.fps = Some(self.frame_count as f32 / elapsed.as_secs_f32());
self.frame_count = 0;
self.last_instant = Instant::now();
}
}
}
impl Default for Fps {
fn default() -> Self {
Self {
frame_count: 0,
last_instant: Instant::now(),
fps: None,
}
}
}
impl<'a> AppWidget<'a> {
fn new(app: &'a App) -> Self {
let title =
Paragraph::new("colors_rgb example. Press q to quit").alignment(Alignment::Center);
Self {
title,
fps_widget: FpsWidget { fps: &app.fps },
rgb_colors_widget: RgbColorsWidget {
colors: &app.colors,
frame_count: app.frame_count,
},
}
}
}
impl Widget for AppWidget<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let main_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(0)])
.split(area);
let title_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(0), Constraint::Length(8)])
.split(main_layout[0]);
self.title.render(title_layout[0], buf);
self.fps_widget.render(title_layout[1], buf);
self.rgb_colors_widget.render(main_layout[1], buf);
}
}
impl Widget for RgbColorsWidget<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let colors = self.colors;
for (xi, x) in (area.left()..area.right()).enumerate() {
// animate the colors by shifting the x index by the frame number
let xi = (xi + self.frame_count) % (area.width as usize);
for (yi, y) in (area.top()..area.bottom()).enumerate() {
let fg = colors[yi * 2][xi];
let bg = colors[yi * 2 + 1][xi];
buf.get_mut(x, y).set_char('▀').set_fg(fg).set_bg(bg);
}
}
}
}
impl<'a> Widget for FpsWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
if let Some(fps) = self.fps.fps {
let text = format!("{:.1} fps", fps);
Paragraph::new(text).render(area, buf);
}
}
}
/// Install a panic hook that restores the terminal before panicking.
fn install_panic_hook() -> color_eyre::Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info)
}));
Ok(())
}
fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
terminal.clear()?;
terminal.hide_cursor()?;
Ok(terminal)
}
fn restore_terminal() -> color_eyre::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

21
examples/colors_rgb.tape Normal file
View File

@@ -0,0 +1,21 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/colors_rgb.tape`
# note that this script sometimes results in the gif having screen tearing
# issues. I'm not sure why, but it's not a problem with the library.
Output "target/colors_rgb.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 1200
# unsure if these help the screen tearing issue, but they don't hurt
Set Framerate 60
Set CursorBlink false
Hide
Type "cargo run --example=colors_rgb --features=crossterm --release"
Enter
Sleep 2s
# Screenshot "target/colors_rgb.png"
Show
Sleep 10s

View File

@@ -1,27 +1,121 @@
use std::{error::Error, io};
use std::{error::Error, io, ops::ControlFlow, time::Duration};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseButton, MouseEvent,
MouseEventKind,
},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
#[derive(Default)]
struct Label<'a> {
text: &'a str,
/// A custom widget that renders a button with a label, theme and state.
#[derive(Debug, Clone)]
struct Button<'a> {
label: Line<'a>,
theme: Theme,
state: State,
}
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());
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum State {
Normal,
Selected,
Active,
}
#[derive(Debug, Clone, Copy)]
struct Theme {
text: Color,
background: Color,
highlight: Color,
shadow: Color,
}
const BLUE: Theme = Theme {
text: Color::Rgb(16, 24, 48),
background: Color::Rgb(48, 72, 144),
highlight: Color::Rgb(64, 96, 192),
shadow: Color::Rgb(32, 48, 96),
};
const RED: Theme = Theme {
text: Color::Rgb(48, 16, 16),
background: Color::Rgb(144, 48, 48),
highlight: Color::Rgb(192, 64, 64),
shadow: Color::Rgb(96, 32, 32),
};
const GREEN: Theme = Theme {
text: Color::Rgb(16, 48, 16),
background: Color::Rgb(48, 144, 48),
highlight: Color::Rgb(64, 192, 64),
shadow: Color::Rgb(32, 96, 32),
};
/// A button with a label that can be themed.
impl<'a> Button<'a> {
pub fn new<T: Into<Line<'a>>>(label: T) -> Button<'a> {
Button {
label: label.into(),
theme: BLUE,
state: State::Normal,
}
}
pub fn theme(mut self, theme: Theme) -> Button<'a> {
self.theme = theme;
self
}
pub fn state(mut self, state: State) -> Button<'a> {
self.state = state;
self
}
}
impl<'a> Label<'a> {
fn text(mut self, text: &'a str) -> Label<'a> {
self.text = text;
self
impl<'a> Widget for Button<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let (background, text, shadow, highlight) = self.colors();
buf.set_style(area, Style::new().bg(background).fg(text));
// render top line if there's enough space
if area.height > 2 {
buf.set_string(
area.x,
area.y,
"".repeat(area.width as usize),
Style::new().fg(highlight).bg(background),
);
}
// render bottom line if there's enough space
if area.height > 1 {
buf.set_string(
area.x,
area.y + area.height - 1,
"".repeat(area.width as usize),
Style::new().fg(shadow).bg(background),
);
}
// render label centered
buf.set_line(
area.x + (area.width.saturating_sub(self.label.width() as u16)) / 2,
area.y + (area.height.saturating_sub(1)) / 2,
&self.label,
area.width,
);
}
}
impl Button<'_> {
fn colors(&self) -> (Color, Color, Color, Color) {
let theme = self.theme;
match self.state {
State::Normal => (theme.background, theme.text, theme.shadow, theme.highlight),
State::Selected => (theme.highlight, theme.text, theme.shadow, theme.highlight),
State::Active => (theme.background, theme.text, theme.highlight, theme.shadow),
}
}
}
@@ -53,19 +147,126 @@ fn main() -> Result<(), Box<dyn Error>> {
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
let mut selected_button: usize = 0;
let button_states = &mut [State::Selected, State::Normal, State::Normal];
loop {
terminal.draw(ui)?;
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
terminal.draw(|frame| ui(frame, button_states))?;
if !event::poll(Duration::from_millis(100))? {
continue;
}
match event::read()? {
Event::Key(key) => {
if key.kind != event::KeyEventKind::Press {
continue;
}
if handle_key_event(key, button_states, &mut selected_button).is_break() {
break;
}
}
Event::Mouse(mouse) => handle_mouse_event(mouse, button_states, &mut selected_button),
_ => (),
}
}
Ok(())
}
fn ui<B: Backend>(f: &mut Frame<B>) {
let size = f.size();
let label = Label::default().text("Test");
f.render_widget(label, size);
fn ui(frame: &mut Frame, states: &[State; 3]) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Max(3),
Constraint::Length(1),
Constraint::Min(0), // ignore remaining space
])
.split(frame.size());
frame.render_widget(
Paragraph::new("Custom Widget Example (mouse enabled)"),
layout[0],
);
render_buttons(frame, layout[1], states);
frame.render_widget(
Paragraph::new("←/→: select, Space: toggle, q: quit"),
layout[2],
);
}
fn render_buttons(frame: &mut Frame<'_>, area: Rect, states: &[State; 3]) {
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length(15),
Constraint::Length(15),
Constraint::Length(15),
Constraint::Min(0), // ignore remaining space
])
.split(area);
frame.render_widget(Button::new("Red").theme(RED).state(states[0]), layout[0]);
frame.render_widget(
Button::new("Green").theme(GREEN).state(states[1]),
layout[1],
);
frame.render_widget(Button::new("Blue").theme(BLUE).state(states[2]), layout[2]);
}
fn handle_key_event(
key: event::KeyEvent,
button_states: &mut [State; 3],
selected_button: &mut usize,
) -> ControlFlow<()> {
match key.code {
KeyCode::Char('q') => return ControlFlow::Break(()),
KeyCode::Left | KeyCode::Char('h') => {
button_states[*selected_button] = State::Normal;
*selected_button = selected_button.saturating_sub(1);
button_states[*selected_button] = State::Selected;
}
KeyCode::Right | KeyCode::Char('l') => {
button_states[*selected_button] = State::Normal;
*selected_button = selected_button.saturating_add(1).min(2);
button_states[*selected_button] = State::Selected;
}
KeyCode::Char(' ') => {
if button_states[*selected_button] == State::Active {
button_states[*selected_button] = State::Normal;
} else {
button_states[*selected_button] = State::Active;
}
}
_ => (),
}
ControlFlow::Continue(())
}
fn handle_mouse_event(
mouse: MouseEvent,
button_states: &mut [State; 3],
selected_button: &mut usize,
) {
match mouse.kind {
MouseEventKind::Moved => {
let old_selected_button = *selected_button;
*selected_button = match mouse.column {
x if x < 15 => 0,
x if x < 30 => 1,
_ => 2,
};
if old_selected_button != *selected_button {
if button_states[old_selected_button] != State::Active {
button_states[old_selected_button] = State::Normal;
}
if button_states[*selected_button] != State::Active {
button_states[*selected_button] = State::Selected;
}
}
}
MouseEventKind::Down(MouseButton::Left) => {
if button_states[*selected_button] == State::Active {
button_states[*selected_button] = State::Normal;
} else {
button_states[*selected_button] = State::Active;
}
}
_ => (),
}
}

View File

@@ -1,11 +1,21 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/custom_widget.tape`
Output "target/custom_widget.gif"
Set Width 1200
Set Height 200
Set Theme "Aardvark Blue"
Set Width 760
Set Height 260
Hide
Type "cargo run --example=custom_widget --features=crossterm"
Enter
Sleep 1s
Sleep 2s
Show
Sleep 5s
Sleep 1s
Set TypingSpeed 0.7s
Right
Right
Space
Space
Left
Space
Left
Space

View File

@@ -1,7 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/demo.tape`
Output "target/demo.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 1200
Set PlaybackSpeed 0.5

View File

@@ -50,18 +50,16 @@ fn run_app<B: Backend>(
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));
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Left | KeyCode::Char('h') => app.on_left(),
KeyCode::Up | KeyCode::Char('k') => app.on_up(),
KeyCode::Right | KeyCode::Char('l') => app.on_right(),
KeyCode::Down | KeyCode::Char('j') => app.on_down(),
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(),
_ => {}
}
}

View File

@@ -39,11 +39,11 @@ fn run_app<B: Backend>(
match events.recv()? {
Event::Input(key) => match key {
Key::Up | Key::Char('k') => app.on_up(),
Key::Down | Key::Char('j') => app.on_down(),
Key::Left | Key::Char('h') => app.on_left(),
Key::Right | Key::Char('l') => app.on_right(),
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(),

View File

@@ -1,6 +1,5 @@
use std::{
error::Error,
io,
time::{Duration, Instant},
};
@@ -32,26 +31,24 @@ fn run_app(
terminal: &mut Terminal<TermwizBackend>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
) -> Result<(), Box<dyn Error>> {
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 let Ok(Some(input)) = terminal
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if let Some(input) = terminal
.backend_mut()
.buffered_terminal_mut()
.terminal()
.poll_input(Some(timeout))
.poll_input(Some(timeout))?
{
match input {
InputEvent::Key(key_code) => match key_code.key {
KeyCode::UpArrow => app.on_up(),
KeyCode::DownArrow => app.on_down(),
KeyCode::LeftArrow => app.on_left(),
KeyCode::RightArrow => app.on_right(),
KeyCode::UpArrow | KeyCode::Char('k') => app.on_up(),
KeyCode::DownArrow | KeyCode::Char('j') => app.on_down(),
KeyCode::LeftArrow | KeyCode::Char('h') => app.on_left(),
KeyCode::RightArrow | KeyCode::Char('l') => app.on_right(),
KeyCode::Char(c) => app.on_key(c),
_ => {}
},

View File

@@ -5,9 +5,9 @@ use ratatui::{
use crate::app::App;
pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
pub fn draw(f: &mut Frame, app: &mut App) {
let chunks = Layout::default()
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
.constraints([Constraint::Length(3), Constraint::Min(0)])
.split(f.size());
let titles = app
.tabs
@@ -28,38 +28,26 @@ pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
};
}
fn draw_first_tab<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
where
B: Backend,
{
fn draw_first_tab(f: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::default()
.constraints(
[
Constraint::Length(9),
Constraint::Min(8),
Constraint::Length(7),
]
.as_ref(),
)
.constraints([
Constraint::Length(9),
Constraint::Min(8),
Constraint::Length(7),
])
.split(area);
draw_gauges(f, app, chunks[0]);
draw_charts(f, app, chunks[1]);
draw_text(f, chunks[2]);
}
fn draw_gauges<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
where
B: Backend,
{
fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::default()
.constraints(
[
Constraint::Length(2),
Constraint::Length(3),
Constraint::Length(1),
]
.as_ref(),
)
.constraints([
Constraint::Length(2),
Constraint::Length(3),
Constraint::Length(1),
])
.margin(1)
.split(area);
let block = Block::default().borders(Borders::ALL).title("Graphs");
@@ -102,10 +90,7 @@ where
f.render_widget(line_gauge, chunks[2]);
}
fn draw_charts<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
where
B: Backend,
{
fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
let constraints = if app.show_chart {
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
} else {
@@ -117,11 +102,11 @@ where
.split(area);
{
let chunks = Layout::default()
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[0]);
{
let chunks = Layout::default()
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.direction(Direction::Horizontal)
.split(chunks[0]);
@@ -249,10 +234,7 @@ where
}
}
fn draw_text<B>(f: &mut Frame<B>, area: Rect)
where
B: Backend,
{
fn draw_text(f: &mut Frame, area: Rect) {
let text = vec![
text::Line::from("This is a paragraph with several lines. You can change style your text the way you want"),
text::Line::from(""),
@@ -290,12 +272,9 @@ where
f.render_widget(paragraph, area);
}
fn draw_second_tab<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
where
B: Backend,
{
fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::default()
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)].as_ref())
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
.direction(Direction::Horizontal)
.split(area);
let up_style = Style::default().fg(Color::Green);
@@ -310,18 +289,20 @@ where
};
Row::new(vec![s.name, s.location, s.status]).style(style)
});
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))
.widths(&[
let table = Table::new(
rows,
[
Constraint::Length(15),
Constraint::Length(15),
Constraint::Length(10),
]);
],
)
.header(
Row::new(vec!["Server", "Location", "Status"])
.style(Style::default().fg(Color::Yellow))
.bottom_margin(1),
)
.block(Block::default().title("Servers").borders(Borders::ALL));
f.render_widget(table, chunks[0]);
let map = Canvas::default()
@@ -379,10 +360,7 @@ where
f.render_widget(map, chunks[1]);
}
fn draw_third_tab<B>(f: &mut Frame<B>, _app: &mut App, area: Rect)
where
B: Backend,
{
fn draw_third_tab(f: &mut Frame, _app: &mut App, area: Rect) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
@@ -417,12 +395,14 @@ where
Row::new(cells)
})
.collect();
let table = Table::new(items)
.block(Block::default().title("Colors").borders(Borders::ALL))
.widths(&[
let table = Table::new(
items,
[
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
]);
],
)
.block(Block::default().title("Colors").borders(Borders::ALL));
f.render_widget(table, chunks[0]);
}

View File

@@ -0,0 +1,43 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/demo.tape`
Output "target/demo2-social.gif"
Set Theme "Aardvark Blue"
# Github social preview size (1280x640 with 80px padding) and must be < 1MB
# This puts some constraints on the amount of interactivity we can do.
Set Width 1280
Set Height 640
Set Padding 80
Hide
Type "cargo run --example demo2 --features crossterm,widget-calendar"
Enter
Sleep 2s
Show
# About screen
Sleep 3.5s
Down # Red eye
Sleep 0.4s
Down # black eye
Sleep 1s
Tab
# Recipe
Sleep 1s
Set TypingSpeed 500ms
Down 7
Sleep 1s
Tab
# Email
Sleep 2s
Down 4
Sleep 2s
Tab
# Trace route
Sleep 1s
Set TypingSpeed 200ms
Down 10
Sleep 2s
Tab
# Weather
Set TypingSpeed 100ms
Down 40
Sleep 2s

49
examples/demo2.tape Normal file
View File

@@ -0,0 +1,49 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/demo.tape`
# NOTE: Requires VHS 0.6.1 or later for Screenshot support
Output "target/demo2.gif"
Set Theme "Aardvark Blue"
# The reason for this strange size is that the social preview image for this
# demo is 1280x64 with 80 pixels of padding on each side. We want a version
# without the padding for README.md, etc.
Set Width 1120
Set Height 480
Set Padding 0
Hide
Type "cargo run --example demo2 --features crossterm,widget-calendar"
Enter
Sleep 2s
Show
# About screen
Screenshot "target/demo2-about.png"
Sleep 3.5s
Down # Red eye
Sleep 0.4s
Down # black eye
Sleep 1s
Tab
# Recipe
Screenshot "target/demo2-recipe.png"
Sleep 1s
Set TypingSpeed 500ms
Down 7
Sleep 1s
Tab
# Email
Screenshot "target/demo2-email.png"
Sleep 2s
Down 4
Sleep 2s
Tab
# Trace route
Screenshot "target/demo2-trace.png"
Sleep 1s
Set TypingSpeed 200ms
Down 10
Sleep 2s
Tab
# Weather
Screenshot "target/demo2-weather.png"
Set TypingSpeed 100ms
Down 40
Sleep 2s

99
examples/demo2/app.rs Normal file
View File

@@ -0,0 +1,99 @@
use std::time::Duration;
use anyhow::{Context, Result};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use ratatui::prelude::Rect;
use crate::{Root, Term};
#[derive(Debug)]
pub struct App {
term: Term,
should_quit: bool,
context: AppContext,
}
#[derive(Debug, Default, Clone, Copy)]
pub struct AppContext {
pub tab_index: usize,
pub row_index: usize,
}
impl App {
fn new() -> Result<Self> {
Ok(Self {
term: Term::start()?,
should_quit: false,
context: AppContext::default(),
})
}
pub fn run() -> Result<()> {
install_panic_hook();
let mut app = Self::new()?;
while !app.should_quit {
app.draw()?;
app.handle_events()?;
}
Term::stop()?;
Ok(())
}
fn draw(&mut self) -> Result<()> {
self.term
.draw(|frame| frame.render_widget(Root::new(&self.context), frame.size()))
.context("terminal.draw")?;
Ok(())
}
fn handle_events(&mut self) -> Result<()> {
match Term::next_event(Duration::from_millis(16))? {
Some(Event::Key(key)) => self.handle_key_event(key),
Some(Event::Resize(width, height)) => {
Ok(self.term.resize(Rect::new(0, 0, width, height))?)
}
_ => Ok(()),
}
}
fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> {
if key.kind != KeyEventKind::Press {
return Ok(());
}
let context = &mut self.context;
const TAB_COUNT: usize = 5;
match key.code {
KeyCode::Char('q') | KeyCode::Esc => {
self.should_quit = true;
}
KeyCode::Tab | KeyCode::BackTab if key.modifiers.contains(KeyModifiers::SHIFT) => {
let tab_index = context.tab_index + TAB_COUNT; // to wrap around properly
context.tab_index = tab_index.saturating_sub(1) % TAB_COUNT;
context.row_index = 0;
}
KeyCode::Tab | KeyCode::BackTab => {
context.tab_index = context.tab_index.saturating_add(1) % TAB_COUNT;
context.row_index = 0;
}
KeyCode::Up | KeyCode::Char('k') => {
context.row_index = context.row_index.saturating_sub(1);
}
KeyCode::Down | KeyCode::Char('j') => {
context.row_index = context.row_index.saturating_add(1);
}
_ => {}
};
Ok(())
}
}
pub fn install_panic_hook() {
better_panic::install();
let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let _ = Term::stop();
hook(info);
std::process::exit(1);
}));
}

34
examples/demo2/colors.rs Normal file
View File

@@ -0,0 +1,34 @@
use palette::{IntoColor, Okhsv, Srgb};
use ratatui::{prelude::*, widgets::*};
/// A widget that renders a color swatch of RGB colors.
///
/// The widget is rendered as a rectangle with the hue changing along the x-axis from 0.0 to 360.0
/// and the value changing along the y-axis (from 1.0 to 0.0). Each pixel is rendered as a block
/// character with the top half slightly lighter than the bottom half.
pub struct RgbSwatch;
impl Widget for RgbSwatch {
fn render(self, area: Rect, buf: &mut Buffer) {
for (yi, y) in (area.top()..area.bottom()).enumerate() {
let value = area.height as f32 - yi as f32;
let value_fg = value / (area.height as f32);
let value_bg = (value - 0.5) / (area.height as f32);
for (xi, x) in (area.left()..area.right()).enumerate() {
let hue = xi as f32 * 360.0 / area.width as f32;
let fg = color_from_oklab(hue, Okhsv::max_saturation(), value_fg);
let bg = color_from_oklab(hue, Okhsv::max_saturation(), value_bg);
buf.get_mut(x, y).set_char('▀').set_fg(fg).set_bg(bg);
}
}
}
}
/// Convert a hue and value into an RGB color via the OkLab color space.
///
/// See <https://bottosson.github.io/posts/oklab/> for more details.
pub fn color_from_oklab(hue: f32, saturation: f32, value: f32) -> Color {
let color: Srgb = Okhsv::new(hue, saturation, value).into_color();
let color = color.into_format();
Color::Rgb(color.red, color.green, color.blue)
}

17
examples/demo2/main.rs Normal file
View File

@@ -0,0 +1,17 @@
use anyhow::Result;
pub use app::*;
pub use colors::*;
pub use root::*;
pub use term::*;
pub use theme::*;
mod app;
mod colors;
mod root;
mod tabs;
mod term;
mod theme;
fn main() -> Result<()> {
App::run()
}

93
examples/demo2/root.rs Normal file
View File

@@ -0,0 +1,93 @@
use std::rc::Rc;
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use crate::{tabs::*, AppContext, THEME};
pub struct Root<'a> {
context: &'a AppContext,
}
impl<'a> Root<'a> {
pub fn new(context: &'a AppContext) -> Self {
Root { context }
}
}
impl Widget for Root<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
Block::new().style(THEME.root).render(area, buf);
let area = layout(area, Direction::Vertical, vec![1, 0, 1]);
self.render_title_bar(area[0], buf);
self.render_selected_tab(area[1], buf);
self.render_bottom_bar(area[2], buf);
}
}
impl Root<'_> {
fn render_title_bar(&self, area: Rect, buf: &mut Buffer) {
let area = layout(area, Direction::Horizontal, vec![0, 45]);
Paragraph::new(Span::styled("Ratatui", THEME.app_title)).render(area[0], buf);
let titles = vec!["", " Recipe ", " Email ", " Traceroute ", " Weather "];
Tabs::new(titles)
.style(THEME.tabs)
.highlight_style(THEME.tabs_selected)
.select(self.context.tab_index)
.divider("")
.render(area[1], buf);
}
fn render_selected_tab(&self, area: Rect, buf: &mut Buffer) {
let row_index = self.context.row_index;
match self.context.tab_index {
0 => AboutTab::new(row_index).render(area, buf),
1 => RecipeTab::new(row_index).render(area, buf),
2 => EmailTab::new(row_index).render(area, buf),
3 => TracerouteTab::new(row_index).render(area, buf),
4 => WeatherTab::new(row_index).render(area, buf),
_ => unreachable!(),
};
}
fn render_bottom_bar(&self, area: Rect, buf: &mut Buffer) {
let keys = [
("Q/Esc", "Quit"),
("Tab", "Next Tab"),
("↑/k", "Up"),
("↓/j", "Down"),
];
let spans = keys
.iter()
.flat_map(|(key, desc)| {
let key = Span::styled(format!(" {} ", key), THEME.key_binding.key);
let desc = Span::styled(format!(" {} ", desc), THEME.key_binding.description);
[key, desc]
})
.collect_vec();
Paragraph::new(Line::from(spans))
.alignment(Alignment::Center)
.fg(Color::Indexed(236))
.bg(Color::Indexed(232))
.render(area, buf);
}
}
/// simple helper method to split an area into multiple sub-areas
pub fn layout(area: Rect, direction: Direction, heights: Vec<u16>) -> Rc<[Rect]> {
let constraints = heights
.iter()
.map(|&h| {
if h > 0 {
Constraint::Length(h)
} else {
Constraint::Min(0)
}
})
.collect_vec();
Layout::default()
.direction(direction)
.constraints(constraints)
.split(area)
}

11
examples/demo2/tabs.rs Normal file
View File

@@ -0,0 +1,11 @@
mod about;
mod email;
mod recipe;
mod traceroute;
mod weather;
pub use about::AboutTab;
pub use email::EmailTab;
pub use recipe::RecipeTab;
pub use traceroute::TracerouteTab;
pub use weather::WeatherTab;

View File

@@ -0,0 +1,157 @@
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use crate::{layout, RgbSwatch, THEME};
const RATATUI_LOGO: [&str; 32] = [
" ███ ",
" ██████ ",
" ███████ ",
" ████████ ",
" █████████ ",
" ██████████ ",
" ████████████ ",
" █████████████ ",
" █████████████ ██████",
" ███████████ ████████",
" █████ ███████████ ",
" ███ ██ee████████ ",
" █ █████████████ ",
" ████ █████████████ ",
" █████████████████ ",
" ████████████████ ",
" ████████████████ ",
" ███ ██████████ ",
" ██ █████████ ",
" █xx█ █████████ ",
" █xxxx█ ██████████ ",
" █xx█xxx█ █████████ ",
" █xx██xxxx█ ████████ ",
" █xxxxxxxxxx█ ██████████ ",
" █xxxxxxxxxxxx█ ██████████ ",
" █xxxxxxx██xxxxx█ █████████ ",
" █xxxxxxxxx██xxxxx█ ████ ███ ",
" █xxxxxxxxxxxxxxxxxx█ ██ ███ ",
"█xxxxxxxxxxxxxxxxxxxx█ █ ███ ",
"█xxxxxxxxxxxxxxxxxxxxx█ ███ ",
" █xxxxxxxxxxxxxxxxxxxxx█ ███ ",
" █xxxxxxxxxxxxxxxxxxxxx█ █ ",
];
pub struct AboutTab {
selected_row: usize,
}
impl AboutTab {
pub fn new(selected_row: usize) -> Self {
Self { selected_row }
}
}
impl Widget for AboutTab {
fn render(self, area: Rect, buf: &mut Buffer) {
RgbSwatch.render(area, buf);
let area = layout(area, Direction::Horizontal, vec![34, 0]);
render_crate_description(area[1], buf);
render_logo(self.selected_row, area[0], buf);
}
}
fn render_crate_description(area: Rect, buf: &mut Buffer) {
let area = area.inner(
&(Margin {
vertical: 4,
horizontal: 2,
}),
);
Clear.render(area, buf); // clear out the color swatches
Block::new().style(THEME.content).render(area, buf);
let area = area.inner(
&(Margin {
vertical: 1,
horizontal: 2,
}),
);
let text = "- cooking up terminal user interfaces -
Ratatui is a Rust crate that provides widgets (e.g. Paragraph, Table) and draws them to the \
screen efficiently every frame.";
Paragraph::new(text)
.style(THEME.description)
.block(
Block::new()
.title(" Ratatui ")
.title_alignment(Alignment::Center)
.borders(Borders::TOP)
.border_style(THEME.description_title)
.padding(Padding::new(0, 0, 0, 0)),
)
.wrap(Wrap { trim: true })
.scroll((0, 0))
.render(area, buf);
}
/// Use half block characters to render a logo based on the RATATUI_LOGO const.
///
/// The logo is rendered in three colors, one for the rat, one for the terminal, and one for the
/// rat's eye. The eye color alternates between two colors based on the selected row.
pub fn render_logo(selected_row: usize, area: Rect, buf: &mut Buffer) {
let eye_color = if selected_row % 2 == 0 {
THEME.logo.rat_eye
} else {
THEME.logo.rat_eye_alt
};
let area = area.inner(&Margin {
vertical: 0,
horizontal: 2,
});
for (y, (line1, line2)) in RATATUI_LOGO.iter().tuples().enumerate() {
for (x, (ch1, ch2)) in line1.chars().zip(line2.chars()).enumerate() {
let x = area.left() + x as u16;
let y = area.top() + y as u16;
let cell = buf.get_mut(x, y);
let rat_color = THEME.logo.rat;
let term_color = THEME.logo.term;
match (ch1, ch2) {
('█', '█') => {
cell.set_char('█');
cell.fg = rat_color;
}
('█', ' ') => {
cell.set_char('▀');
cell.fg = rat_color;
}
(' ', '█') => {
cell.set_char('▄');
cell.fg = rat_color;
}
('█', 'x') => {
cell.set_char('▀');
cell.fg = rat_color;
cell.bg = term_color;
}
('x', '█') => {
cell.set_char('▄');
cell.fg = rat_color;
cell.bg = term_color;
}
('x', 'x') => {
cell.set_char(' ');
cell.fg = term_color;
cell.bg = term_color;
}
('█', 'e') => {
cell.set_char('▀');
cell.fg = rat_color;
cell.bg = eye_color;
}
('e', '█') => {
cell.set_char('▄');
cell.fg = rat_color;
cell.bg = eye_color;
}
(_, _) => {}
};
}
}
}

View File

@@ -0,0 +1,143 @@
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use unicode_width::UnicodeWidthStr;
use crate::{layout, RgbSwatch, THEME};
#[derive(Debug, Default)]
pub struct Email {
from: &'static str,
subject: &'static str,
body: &'static str,
}
const EMAILS: &[Email] = &[
Email {
from: "Alice <alice@example.com>",
subject: "Hello",
body: "Hi Bob,\nHow are you?\n\nAlice",
},
Email {
from: "Bob <bob@example.com>",
subject: "Re: Hello",
body: "Hi Alice,\nI'm fine, thanks!\n\nBob",
},
Email {
from: "Charlie <charlie@example.com>",
subject: "Re: Hello",
body: "Hi Alice,\nI'm fine, thanks!\n\nCharlie",
},
Email {
from: "Dave <dave@example.com>",
subject: "Re: Hello (STOP REPLYING TO ALL)",
body: "Hi Everyone,\nPlease stop replying to all.\n\nDave",
},
Email {
from: "Eve <eve@example.com>",
subject: "Re: Hello (STOP REPLYING TO ALL)",
body: "Hi Everyone,\nI'm reading all your emails.\n\nEve",
},
];
#[derive(Debug, Default)]
pub struct EmailTab {
selected_index: usize,
}
impl EmailTab {
pub fn new(selected_index: usize) -> Self {
Self {
selected_index: selected_index % EMAILS.len(),
}
}
}
impl Widget for EmailTab {
fn render(self, area: Rect, buf: &mut Buffer) {
RgbSwatch.render(area, buf);
let area = area.inner(&Margin {
vertical: 1,
horizontal: 2,
});
Clear.render(area, buf);
let area = layout(area, Direction::Vertical, vec![5, 0]);
render_inbox(self.selected_index, area[0], buf);
render_email(self.selected_index, area[1], buf);
}
}
fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
let area = layout(area, Direction::Vertical, vec![1, 0]);
let theme = THEME.email;
Tabs::new(vec![" Inbox ", " Sent ", " Drafts "])
.style(theme.tabs)
.highlight_style(theme.tabs_selected)
.select(0)
.divider("")
.render(area[0], buf);
let highlight_symbol = ">>";
let from_width = EMAILS
.iter()
.map(|e| e.from.width())
.max()
.unwrap_or_default();
let items = EMAILS
.iter()
.map(|e| {
let from = format!("{:width$}", e.from, width = from_width).into();
ListItem::new(Line::from(vec![from, " ".into(), e.subject.into()]))
})
.collect_vec();
let mut state = ListState::default().with_selected(Some(selected_index));
StatefulWidget::render(
List::new(items)
.style(theme.inbox)
.highlight_style(theme.selected_item)
.highlight_symbol(highlight_symbol),
area[1],
buf,
&mut state,
);
let mut scrollbar_state = ScrollbarState::default()
.content_length(EMAILS.len())
.position(selected_index);
Scrollbar::default()
.begin_symbol(None)
.end_symbol(None)
.track_symbol(None)
.thumb_symbol("")
.render(area[1], buf, &mut scrollbar_state);
}
fn render_email(selected_index: usize, area: Rect, buf: &mut Buffer) {
let theme = THEME.email;
let email = EMAILS.get(selected_index);
let block = Block::new()
.style(theme.body)
.padding(Padding::new(2, 2, 0, 0))
.borders(Borders::TOP)
.border_type(BorderType::Thick);
let inner = block.inner(area);
block.render(area, buf);
if let Some(email) = email {
let area = layout(inner, Direction::Vertical, vec![3, 0]);
let headers = vec![
Line::from(vec![
"From: ".set_style(theme.header),
email.from.set_style(theme.header_value),
]),
Line::from(vec![
"Subject: ".set_style(theme.header),
email.subject.set_style(theme.header_value),
]),
"-".repeat(inner.width as usize).dim().into(),
];
Paragraph::new(headers)
.style(theme.body)
.render(area[0], buf);
let body = email.body.lines().map(Line::from).collect_vec();
Paragraph::new(body).style(theme.body).render(area[1], buf);
} else {
Paragraph::new("No email selected").render(inner, buf);
}
}

View File

@@ -0,0 +1,170 @@
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use crate::{layout, RgbSwatch, THEME};
#[derive(Debug, Default, Clone, Copy)]
struct Ingredient {
quantity: &'static str,
name: &'static str,
}
impl Ingredient {
fn height(&self) -> u16 {
self.name.lines().count() as u16
}
}
impl<'a> From<Ingredient> for Row<'a> {
fn from(i: Ingredient) -> Self {
Row::new(vec![i.quantity, i.name]).height(i.height())
}
}
// https://www.realsimple.com/food-recipes/browse-all-recipes/ratatouille
const RECIPE: &[(&str, &str)] = &[
(
"Step 1: ",
"Over medium-low heat, add the oil to a large skillet with the onion, garlic, and bay \
leaf, stirring occasionally, until the onion has softened.",
),
(
"Step 2: ",
"Add the eggplant and cook, stirring occasionally, for 8 minutes or until the eggplant \
has softened. Stir in the zucchini, red bell pepper, tomatoes, and salt, and cook over \
medium heat, stirring occasionally, for 5 to 7 minutes or until the vegetables are \
tender. Stir in the basil and few grinds of pepper to taste.",
),
];
const INGREDIENTS: &[Ingredient] = &[
Ingredient {
quantity: "4 tbsp",
name: "olive oil",
},
Ingredient {
quantity: "1",
name: "onion thinly sliced",
},
Ingredient {
quantity: "4",
name: "cloves garlic\npeeled and sliced",
},
Ingredient {
quantity: "1",
name: "small bay leaf",
},
Ingredient {
quantity: "1",
name: "small eggplant cut\ninto 1/2 inch cubes",
},
Ingredient {
quantity: "1",
name: "small zucchini halved\nlengthwise and cut\ninto thin slices",
},
Ingredient {
quantity: "1",
name: "red bell pepper cut\ninto slivers",
},
Ingredient {
quantity: "4",
name: "plum tomatoes\ncoarsely chopped",
},
Ingredient {
quantity: "1 tsp",
name: "kosher salt",
},
Ingredient {
quantity: "1/4 cup",
name: "shredded fresh basil\nleaves",
},
Ingredient {
quantity: "",
name: "freshly ground black\npepper",
},
];
#[derive(Debug)]
pub struct RecipeTab {
selected_row: usize,
}
impl RecipeTab {
pub fn new(selected_row: usize) -> Self {
Self {
selected_row: selected_row % INGREDIENTS.len(),
}
}
}
impl Widget for RecipeTab {
fn render(self, area: Rect, buf: &mut Buffer) {
RgbSwatch.render(area, buf);
let area = area.inner(&Margin {
vertical: 1,
horizontal: 2,
});
Clear.render(area, buf);
Block::new()
.title("Ratatouille Recipe".bold().white())
.title_alignment(Alignment::Center)
.style(THEME.content)
.padding(Padding::new(1, 1, 2, 1))
.render(area, buf);
let scrollbar_area = Rect {
y: area.y + 2,
height: area.height - 3,
..area
};
render_scrollbar(self.selected_row, scrollbar_area, buf);
let area = area.inner(&Margin {
horizontal: 2,
vertical: 1,
});
let area = layout(area, Direction::Horizontal, vec![44, 0]);
render_recipe(area[0], buf);
render_ingredients(self.selected_row, area[1], buf);
}
}
fn render_recipe(area: Rect, buf: &mut Buffer) {
let lines = RECIPE
.iter()
.map(|(step, text)| Line::from(vec![step.white().bold(), text.gray()]))
.collect_vec();
Paragraph::new(lines)
.wrap(Wrap { trim: true })
.block(Block::new().padding(Padding::new(0, 1, 0, 0)))
.render(area, buf);
}
fn render_ingredients(selected_row: usize, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default().with_selected(Some(selected_row));
let rows = INGREDIENTS.iter().map(|&i| i.into()).collect_vec();
let theme = THEME.recipe;
StatefulWidget::render(
Table::new(rows, [Constraint::Length(7), Constraint::Length(30)])
.block(Block::new().style(theme.ingredients))
.header(Row::new(vec!["Qty", "Ingredient"]).style(theme.ingredients_header))
.highlight_style(Style::new().light_yellow()),
area,
buf,
&mut state,
);
}
fn render_scrollbar(position: usize, area: Rect, buf: &mut Buffer) {
let mut state = ScrollbarState::default()
.content_length(INGREDIENTS.len())
.viewport_content_length(6)
.position(position);
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(None)
.end_symbol(None)
.track_symbol(None)
.thumb_symbol("")
.render(area, buf, &mut state)
}

View File

@@ -0,0 +1,198 @@
use itertools::Itertools;
use ratatui::{
prelude::*,
widgets::{canvas::*, *},
};
use crate::{layout, RgbSwatch, THEME};
#[derive(Debug)]
pub struct TracerouteTab {
selected_row: usize,
}
impl TracerouteTab {
pub fn new(selected_row: usize) -> Self {
Self {
selected_row: selected_row % HOPS.len(),
}
}
}
impl Widget for TracerouteTab {
fn render(self, area: Rect, buf: &mut Buffer) {
RgbSwatch.render(area, buf);
let area = area.inner(&Margin {
vertical: 1,
horizontal: 2,
});
Clear.render(area, buf);
Block::new().style(THEME.content).render(area, buf);
let area = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
.split(area);
let left_area = layout(area[0], Direction::Vertical, vec![0, 3]);
render_hops(self.selected_row, left_area[0], buf);
render_ping(self.selected_row, left_area[1], buf);
render_map(self.selected_row, area[1], buf);
}
}
fn render_hops(selected_row: usize, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default().with_selected(Some(selected_row));
let rows = HOPS
.iter()
.map(|hop| Row::new(vec![hop.host, hop.address]))
.collect_vec();
let block = Block::default()
.title("Traceroute bad.horse".bold().white())
.title_alignment(Alignment::Center)
.padding(Padding::new(1, 1, 1, 1));
StatefulWidget::render(
Table::new(rows, [Constraint::Max(100), Constraint::Length(15)])
.header(Row::new(vec!["Host", "Address"]).set_style(THEME.traceroute.header))
.highlight_style(THEME.traceroute.selected)
.block(block),
area,
buf,
&mut state,
);
let mut scrollbar_state = ScrollbarState::default()
.content_length(HOPS.len())
.position(selected_row);
let area = Rect {
width: area.width + 1,
y: area.y + 3,
height: area.height - 4,
..area
};
Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalLeft)
.begin_symbol(None)
.end_symbol(None)
.track_symbol(None)
.thumb_symbol("")
.render(area, buf, &mut scrollbar_state);
}
pub fn render_ping(progress: usize, area: Rect, buf: &mut Buffer) {
let mut data = [
8, 8, 8, 8, 7, 7, 7, 6, 6, 5, 4, 3, 3, 2, 2, 1, 1, 1, 2, 2, 3, 4, 5, 6, 7, 7, 8, 8, 8, 7,
7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 2, 4, 6, 7, 8, 8, 8, 8, 6, 4, 2, 1, 1, 1, 1, 2, 2, 2, 3,
3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7,
];
let mid = progress % data.len();
data.rotate_left(mid);
Sparkline::default()
.block(
Block::new()
.title("Ping")
.title_alignment(Alignment::Center)
.border_type(BorderType::Thick),
)
.data(&data)
.style(THEME.traceroute.ping)
.render(area, buf);
}
fn render_map(selected_row: usize, area: Rect, buf: &mut Buffer) {
let theme = THEME.traceroute.map;
let path: Option<(&Hop, &Hop)> = HOPS.iter().tuple_windows().nth(selected_row);
let map = Map {
resolution: canvas::MapResolution::High,
color: theme.color,
};
Canvas::default()
.background_color(theme.background_color)
.block(
Block::new()
.padding(Padding::new(1, 0, 1, 0))
.style(theme.style),
)
.marker(Marker::HalfBlock)
// picked to show Australia for the demo as it's the most interesting part of the map
// (and the only part with hops ;))
.x_bounds([112.0, 155.0])
.y_bounds([-46.0, -11.0])
.paint(|context| {
context.draw(&map);
if let Some(path) = path {
context.draw(&canvas::Line::new(
path.0.location.0,
path.0.location.1,
path.1.location.0,
path.1.location.1,
theme.path,
));
context.draw(&Points {
color: theme.source,
coords: &[path.0.location], // sydney
});
context.draw(&Points {
color: theme.destination,
coords: &[path.1.location], // perth
});
}
})
.render(area, buf);
}
#[derive(Debug)]
struct Hop {
host: &'static str,
address: &'static str,
location: (f64, f64),
}
impl Hop {
const fn new(name: &'static str, address: &'static str, location: (f64, f64)) -> Self {
Self {
host: name,
address,
location,
}
}
}
const CANBERRA: (f64, f64) = (149.1, -35.3);
const SYDNEY: (f64, f64) = (151.1, -33.9);
const MELBOURNE: (f64, f64) = (144.9, -37.8);
const PERTH: (f64, f64) = (115.9, -31.9);
const DARWIN: (f64, f64) = (130.8, -12.4);
const BRISBANE: (f64, f64) = (153.0, -27.5);
const ADELAIDE: (f64, f64) = (138.6, -34.9);
// Go traceroute bad.horse some time, it's fun. these locations are made up and don't correspond
// to the actual IP addresses (which are in Toronto, Canada).
const HOPS: &[Hop] = &[
Hop::new("home", "127.0.0.1", CANBERRA),
Hop::new("bad.horse", "162.252.205.130", SYDNEY),
Hop::new("bad.horse", "162.252.205.131", MELBOURNE),
Hop::new("bad.horse", "162.252.205.132", BRISBANE),
Hop::new("bad.horse", "162.252.205.133", SYDNEY),
Hop::new("he.rides.across.the.nation", "162.252.205.134", PERTH),
Hop::new("the.thoroughbred.of.sin", "162.252.205.135", DARWIN),
Hop::new("he.got.the.application", "162.252.205.136", BRISBANE),
Hop::new("that.you.just.sent.in", "162.252.205.137", ADELAIDE),
Hop::new("it.needs.evaluation", "162.252.205.138", DARWIN),
Hop::new("so.let.the.games.begin", "162.252.205.139", PERTH),
Hop::new("a.heinous.crime", "162.252.205.140", BRISBANE),
Hop::new("a.show.of.force", "162.252.205.141", CANBERRA),
Hop::new("a.murder.would.be.nice.of.course", "162.252.205.142", PERTH),
Hop::new("bad.horse", "162.252.205.143", MELBOURNE),
Hop::new("bad.horse", "162.252.205.144", DARWIN),
Hop::new("bad.horse", "162.252.205.145", MELBOURNE),
Hop::new("he-s.bad", "162.252.205.146", PERTH),
Hop::new("the.evil.league.of.evil", "162.252.205.147", BRISBANE),
Hop::new("is.watching.so.beware", "162.252.205.148", DARWIN),
Hop::new("the.grade.that.you.receive", "162.252.205.149", PERTH),
Hop::new("will.be.your.last.we.swear", "162.252.205.150", ADELAIDE),
Hop::new("so.make.the.bad.horse.gleeful", "162.252.205.151", SYDNEY),
Hop::new("or.he-ll.make.you.his.mare", "162.252.205.152", MELBOURNE),
Hop::new("o_o", "162.252.205.153", BRISBANE),
Hop::new("you-re.saddled.up", "162.252.205.154", DARWIN),
Hop::new("there-s.no.recourse", "162.252.205.155", PERTH),
Hop::new("it-s.hi-ho.silver", "162.252.205.156", SYDNEY),
Hop::new("signed.bad.horse", "162.252.205.157", CANBERRA),
];

View File

@@ -0,0 +1,141 @@
use itertools::Itertools;
use palette::Okhsv;
use ratatui::{
prelude::*,
widgets::{calendar::CalendarEventStore, *},
};
use time::OffsetDateTime;
use crate::{color_from_oklab, layout, RgbSwatch, THEME};
pub struct WeatherTab {
pub selected_row: usize,
}
impl WeatherTab {
pub fn new(selected_row: usize) -> Self {
Self { selected_row }
}
}
impl Widget for WeatherTab {
fn render(self, area: Rect, buf: &mut Buffer) {
RgbSwatch.render(area, buf);
let area = area.inner(&Margin {
vertical: 1,
horizontal: 2,
});
Clear.render(area, buf);
Block::new().style(THEME.content).render(area, buf);
let area = area.inner(&Margin {
horizontal: 2,
vertical: 1,
});
let area = layout(area, Direction::Vertical, vec![0, 1, 1]);
render_gauges(self.selected_row, area[2], buf);
let area = layout(area[0], Direction::Horizontal, vec![23, 0]);
render_calendar(area[0], buf);
let area = layout(area[1], Direction::Horizontal, vec![29, 0]);
render_simple_barchart(area[0], buf);
render_horizontal_barchart(area[1], buf);
}
}
fn render_calendar(area: Rect, buf: &mut Buffer) {
let date = OffsetDateTime::now_utc().date();
calendar::Monthly::new(date, CalendarEventStore::today(Style::new().red().bold()))
.block(Block::new().padding(Padding::new(0, 0, 2, 0)))
.show_month_header(Style::new().bold())
.show_weekdays_header(Style::new().italic())
.render(area, buf);
}
fn render_simple_barchart(area: Rect, buf: &mut Buffer) {
let data = [
("Sat", 76),
("Sun", 69),
("Mon", 65),
("Tue", 67),
("Wed", 65),
("Thu", 69),
("Fri", 73),
];
let data = data
.into_iter()
.map(|(label, value)| {
Bar::default()
.value(value)
// This doesn't actually render correctly as the text is too wide for the bar
// See https://github.com/ratatui-org/ratatui/issues/513 for more info
// (the demo GIFs hack around this by hacking the calculation in bars.rs)
.text_value(format!("{}°", value))
.style(if value > 70 {
Style::new().fg(Color::Red)
} else {
Style::new().fg(Color::Yellow)
})
.value_style(if value > 70 {
Style::new().fg(Color::Gray).bg(Color::Red).bold()
} else {
Style::new().fg(Color::DarkGray).bg(Color::Yellow).bold()
})
.label(label.into())
})
.collect_vec();
let group = BarGroup::default().bars(&data);
BarChart::default()
.data(group)
.bar_width(3)
.bar_gap(1)
.render(area, buf);
}
fn render_horizontal_barchart(area: Rect, buf: &mut Buffer) {
let bg = Color::Rgb(32, 48, 96);
let data = [
Bar::default().text_value("Winter 37-51".into()).value(51),
Bar::default().text_value("Spring 40-65".into()).value(65),
Bar::default().text_value("Summer 54-77".into()).value(77),
Bar::default()
.text_value("Fall 41-71".into())
.value(71)
.value_style(Style::new().bold()), // current season
];
let group = BarGroup::default().label("GPU".into()).bars(&data);
BarChart::default()
.block(Block::new().padding(Padding::new(0, 0, 2, 0)))
.direction(Direction::Horizontal)
.data(group)
.bar_gap(1)
.bar_style(Style::new().fg(bg))
.value_style(Style::new().bg(bg).fg(Color::Gray))
.render(area, buf);
}
pub fn render_gauges(progress: usize, area: Rect, buf: &mut Buffer) {
let percent = (progress * 3).min(100) as f64;
render_line_gauge(percent, area, buf);
}
fn render_line_gauge(percent: f64, area: Rect, buf: &mut Buffer) {
// cycle color hue based on the percent for a neat effect yellow -> red
let hue = 90.0 - (percent as f32 * 0.6);
let value = Okhsv::max_value();
let fg = color_from_oklab(hue, Okhsv::max_saturation(), value);
let bg = color_from_oklab(hue, Okhsv::max_saturation(), value * 0.5);
let label = if percent < 100.0 {
format!("Downloading: {}%", percent)
} else {
"Download Complete!".into()
};
LineGauge::default()
.ratio(percent / 100.0)
.label(label)
.style(Style::new().light_blue())
.gauge_style(Style::new().fg(fg).bg(bg))
.line_set(symbols::line::THICK)
.render(area, buf);
}

71
examples/demo2/term.rs Normal file
View File

@@ -0,0 +1,71 @@
use std::{
io::{self, stdout, Stdout},
ops::{Deref, DerefMut},
time::Duration,
};
use anyhow::{Context, Result};
use crossterm::{
event::{self, Event},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::prelude::*;
/// A wrapper around the terminal that handles setting up and tearing down the terminal
/// and provides a helper method to read events from the terminal.
#[derive(Debug)]
pub struct Term {
terminal: Terminal<CrosstermBackend<Stdout>>,
}
impl Term {
pub fn start() -> Result<Self> {
// this size is to match the size of the terminal when running the demo
// using vhs in a 1280x640 sized window (github social preview size)
let options = TerminalOptions {
viewport: Viewport::Fixed(Rect::new(0, 0, 81, 18)),
};
let terminal = Terminal::with_options(CrosstermBackend::new(io::stdout()), options)?;
enable_raw_mode().context("enable raw mode")?;
stdout()
.execute(EnterAlternateScreen)
.context("enter alternate screen")?;
Ok(Self { terminal })
}
pub fn stop() -> Result<()> {
disable_raw_mode().context("disable raw mode")?;
stdout()
.execute(LeaveAlternateScreen)
.context("leave alternate screen")?;
Ok(())
}
pub fn next_event(timeout: Duration) -> io::Result<Option<Event>> {
if !event::poll(timeout)? {
return Ok(None);
}
let event = event::read()?;
Ok(Some(event))
}
}
impl Deref for Term {
type Target = Terminal<CrosstermBackend<Stdout>>;
fn deref(&self) -> &Self::Target {
&self.terminal
}
}
impl DerefMut for Term {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.terminal
}
}
impl Drop for Term {
fn drop(&mut self) {
let _ = Term::stop();
}
}

136
examples/demo2/theme.rs Normal file
View File

@@ -0,0 +1,136 @@
use ratatui::prelude::*;
pub struct Theme {
pub root: Style,
pub content: Style,
pub app_title: Style,
pub tabs: Style,
pub tabs_selected: Style,
pub borders: Style,
pub description: Style,
pub description_title: Style,
pub key_binding: KeyBinding,
pub logo: Logo,
pub email: Email,
pub traceroute: Traceroute,
pub recipe: Recipe,
}
pub struct KeyBinding {
pub key: Style,
pub description: Style,
}
pub struct Logo {
pub rat: Color,
pub rat_eye: Color,
pub rat_eye_alt: Color,
pub term: Color,
}
pub struct Email {
pub tabs: Style,
pub tabs_selected: Style,
pub inbox: Style,
pub item: Style,
pub selected_item: Style,
pub header: Style,
pub header_value: Style,
pub body: Style,
}
pub struct Traceroute {
pub header: Style,
pub selected: Style,
pub ping: Style,
pub map: Map,
}
pub struct Map {
pub style: Style,
pub color: Color,
pub path: Color,
pub source: Color,
pub destination: Color,
pub background_color: Color,
}
pub struct Recipe {
pub ingredients: Style,
pub ingredients_header: Style,
}
pub const THEME: Theme = Theme {
root: Style::new().bg(DARK_BLUE),
content: Style::new().bg(DARK_BLUE).fg(LIGHT_GRAY),
app_title: Style::new()
.fg(WHITE)
.bg(DARK_BLUE)
.add_modifier(Modifier::BOLD),
tabs: Style::new().fg(MID_GRAY).bg(DARK_BLUE),
tabs_selected: Style::new()
.fg(WHITE)
.bg(DARK_BLUE)
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::REVERSED),
borders: Style::new().fg(LIGHT_GRAY),
description: Style::new().fg(LIGHT_GRAY).bg(DARK_BLUE),
description_title: Style::new().fg(LIGHT_GRAY).add_modifier(Modifier::BOLD),
logo: Logo {
rat: WHITE,
rat_eye: BLACK,
rat_eye_alt: RED,
term: BLACK,
},
key_binding: KeyBinding {
key: Style::new().fg(BLACK).bg(DARK_GRAY),
description: Style::new().fg(DARK_GRAY).bg(BLACK),
},
email: Email {
tabs: Style::new().fg(MID_GRAY).bg(DARK_BLUE),
tabs_selected: Style::new()
.fg(WHITE)
.bg(DARK_BLUE)
.add_modifier(Modifier::BOLD),
inbox: Style::new().bg(DARK_BLUE).fg(LIGHT_GRAY),
item: Style::new().fg(LIGHT_GRAY),
selected_item: Style::new().fg(LIGHT_YELLOW),
header: Style::new().add_modifier(Modifier::BOLD),
header_value: Style::new().fg(LIGHT_GRAY),
body: Style::new().bg(DARK_BLUE).fg(LIGHT_GRAY),
},
traceroute: Traceroute {
header: Style::new()
.bg(DARK_BLUE)
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::UNDERLINED),
selected: Style::new().fg(LIGHT_YELLOW),
ping: Style::new().fg(WHITE),
map: Map {
style: Style::new().bg(DARK_BLUE),
background_color: DARK_BLUE,
color: LIGHT_GRAY,
path: LIGHT_BLUE,
source: LIGHT_GREEN,
destination: LIGHT_RED,
},
},
recipe: Recipe {
ingredients: Style::new().bg(DARK_BLUE).fg(LIGHT_GRAY),
ingredients_header: Style::new()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::UNDERLINED),
},
};
const DARK_BLUE: Color = Color::Rgb(16, 24, 48);
const LIGHT_BLUE: Color = Color::Rgb(64, 96, 192);
const LIGHT_YELLOW: Color = Color::Rgb(192, 192, 96);
const LIGHT_GREEN: Color = Color::Rgb(64, 192, 96);
const LIGHT_RED: Color = Color::Rgb(192, 96, 96);
const RED: Color = Color::Indexed(160);
const BLACK: Color = Color::Indexed(232); // not really black, often #080808
const DARK_GRAY: Color = Color::Indexed(238);
const MID_GRAY: Color = Color::Indexed(244);
const LIGHT_GRAY: Color = Color::Indexed(250);
const WHITE: Color = Color::Indexed(255); // not really white, often #eeeeee

126
examples/docsrs.rs Normal file
View File

@@ -0,0 +1,126 @@
use std::io::{self, stdout};
use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{prelude::*, widgets::*};
/// Example code for libr.rs
///
/// When cargo-rdme supports doc comments that import from code, this will be imported
/// rather than copied to the lib.rs file.
fn main() -> io::Result<()> {
let arg = std::env::args().nth(1).unwrap_or_default();
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
let mut should_quit = false;
while !should_quit {
terminal.draw(match arg.as_str() {
"hello_world" => hello_world,
"layout" => layout,
"styling" => styling,
_ => hello_world,
})?;
should_quit = handle_events()?;
}
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
fn hello_world(frame: &mut Frame) {
frame.render_widget(
Paragraph::new("Hello World!")
.block(Block::default().title("Greeting").borders(Borders::ALL)),
frame.size(),
);
}
use crossterm::event::{self, Event, KeyCode};
fn handle_events() -> io::Result<bool> {
if event::poll(std::time::Duration::from_millis(50))? {
if let Event::Key(key) = event::read()? {
if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(true);
}
}
}
Ok(false)
}
fn layout(frame: &mut Frame) {
let main_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
])
.split(frame.size());
frame.render_widget(
Block::new().borders(Borders::TOP).title("Title Bar"),
main_layout[0],
);
frame.render_widget(
Block::new().borders(Borders::TOP).title("Status Bar"),
main_layout[2],
);
let inner_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(main_layout[1]);
frame.render_widget(
Block::default().borders(Borders::ALL).title("Left"),
inner_layout[0],
);
frame.render_widget(
Block::default().borders(Borders::ALL).title("Right"),
inner_layout[1],
);
}
fn styling(frame: &mut Frame) {
let areas = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
])
.split(frame.size());
let span1 = Span::raw("Hello ");
let span2 = Span::styled(
"World",
Style::new()
.fg(Color::Green)
.bg(Color::White)
.add_modifier(Modifier::BOLD),
);
let span3 = "!".red().on_light_yellow().italic();
let line = Line::from(vec![span1, span2, span3]);
let text: Text = Text::from(vec![line]);
frame.render_widget(Paragraph::new(text), areas[0]);
// or using the short-hand syntax and implicit conversions
frame.render_widget(
Paragraph::new("Hello World!".red().on_white().bold()),
areas[1],
);
// to style the whole widget instead of just the text
frame.render_widget(
Paragraph::new("Hello World!").style(Style::new().red().on_white()),
areas[2],
);
// or using the short-hand syntax
frame.render_widget(Paragraph::new("Hello World!").blue().on_yellow(), areas[3]);
}

39
examples/docsrs.tape Normal file
View File

@@ -0,0 +1,39 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/demo.tape`
# NOTE: Requires VHS 0.6.1 or later for Screenshot support
Output "target/docsrs.gif"
Set Theme "Aardvark Blue"
# The reason for this strange size is that the social preview image for this
# demo is 1280x64 with 80 pixels of padding on each side. We want a version
# without the padding for README.md, etc.
Set Width 640
Set Height 160
Set Padding 0
Hide
Type "cargo run --example docsrs --features crossterm"
Enter
Sleep 2s
Show
Sleep 1s
Screenshot "target/docsrs-hello.png"
Sleep 1s
Hide
Type "q"
Type "cargo run --example docsrs --features crossterm -- layout"
Enter
Sleep 2s
Show
Sleep 1s
Screenshot "target/docsrs-layout.png"
Sleep 1s
Hide
Type "q"
Type "cargo run --example docsrs --features crossterm -- styling"
Enter
Sleep 2s
Show
Sleep 1s
Screenshot "target/docsrs-styling.png"
Sleep 1s
Hide
Type "q"

View File

@@ -1,17 +1,32 @@
use std::{
error::Error,
io,
time::{Duration, Instant},
io::{self, stdout, Stdout},
rc::Rc,
time::Duration,
};
use anyhow::Result;
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{prelude::*, widgets::*};
use ratatui::{
prelude::*,
widgets::{block::Title, *},
};
fn main() -> Result<()> {
App::run()
}
struct App {
term: Term,
should_quit: bool,
state: AppState,
}
#[derive(Debug, Default, Clone, Copy)]
struct AppState {
progress1: u16,
progress2: u16,
progress3: f64,
@@ -19,141 +34,154 @@ struct App {
}
impl App {
fn new() -> App {
App {
progress1: 0,
progress2: 0,
progress3: 0.45,
progress4: 0,
fn run() -> Result<()> {
// run at ~10 fps minus the time it takes to draw
let timeout = Duration::from_secs_f32(1.0 / 10.0);
let mut app = Self::start()?;
while !app.should_quit {
app.update();
app.draw()?;
app.handle_events(timeout)?;
}
app.stop()?;
Ok(())
}
fn on_tick(&mut self) {
self.progress1 += 1;
if self.progress1 > 100 {
self.progress1 = 0;
}
self.progress2 += 2;
if self.progress2 > 100 {
self.progress2 = 0;
}
self.progress3 += 0.001;
if self.progress3 > 1.0 {
self.progress3 = 0.0;
}
self.progress4 += 1;
if self.progress4 > 100 {
self.progress4 = 0;
}
}
}
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 tick_rate = Duration::from_millis(250);
let app = App::new();
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:?}");
fn start() -> Result<Self> {
Ok(App {
term: Term::start()?,
should_quit: false,
state: AppState {
progress1: 0,
progress2: 0,
progress3: 0.0,
progress4: 0,
},
})
}
Ok(())
}
fn stop(&mut self) -> Result<()> {
Term::stop()?;
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))?;
fn update(&mut self) {
self.state.progress1 = (self.state.progress1 + 4).min(100);
self.state.progress2 = (self.state.progress2 + 3).min(100);
self.state.progress3 = (self.state.progress3 + 0.02).min(1.0);
self.state.progress4 = (self.state.progress4 + 1).min(100);
}
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout)? {
fn draw(&mut self) -> Result<()> {
self.term.draw(|frame| {
let state = self.state;
let layout = Self::equal_layout(frame);
Self::render_gauge1(state.progress1, frame, layout[0]);
Self::render_gauge2(state.progress2, frame, layout[1]);
Self::render_gauge3(state.progress3, frame, layout[2]);
Self::render_gauge4(state.progress4, frame, layout[3]);
})?;
Ok(())
}
fn handle_events(&mut self, timeout: Duration) -> io::Result<()> {
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
if key.kind == KeyEventKind::Press {
if let KeyCode::Char('q') = key.code {
self.should_quit = true;
}
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
Ok(())
}
fn equal_layout(frame: &Frame) -> Rc<[Rect]> {
Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
])
.split(frame.size())
}
fn render_gauge1(progress: u16, frame: &mut Frame, area: Rect) {
let title = Self::title_block("Gauge with percentage progress");
let gauge = Gauge::default()
.block(title)
.gauge_style(Style::new().light_red())
.percent(progress);
frame.render_widget(gauge, area);
}
fn render_gauge2(progress: u16, frame: &mut Frame, area: Rect) {
let title = Self::title_block("Gauge with percentage progress and custom label");
let label = format!("{}/100", progress);
let gauge = Gauge::default()
.block(title)
.gauge_style(Style::new().blue().on_light_blue())
.percent(progress)
.label(label);
frame.render_widget(gauge, area);
}
fn render_gauge3(progress: f64, frame: &mut Frame, area: Rect) {
let title =
Self::title_block("Gauge with ratio progress, custom label with style, and unicode");
let label = Span::styled(
format!("{:.2}%", progress * 100.0),
Style::new().red().italic().bold(),
);
let gauge = Gauge::default()
.block(title)
.gauge_style(Style::default().fg(Color::Yellow))
.ratio(progress)
.label(label)
.use_unicode(true);
frame.render_widget(gauge, area);
}
fn render_gauge4(progress: u16, frame: &mut Frame, area: Rect) {
let title = Self::title_block("Gauge with percentage progress and label");
let label = format!("{}/100", progress);
let gauge = Gauge::default()
.block(title)
.gauge_style(Style::new().green().italic())
.percent(progress)
.label(label);
frame.render_widget(gauge, area);
}
fn title_block(title: &str) -> Block {
let title = Title::from(title).alignment(Alignment::Center);
Block::default().title(title).borders(Borders::TOP)
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.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.progress4);
let gauge = Gauge::default()
.block(Block::default().title("Gauge4").borders(Borders::ALL))
.gauge_style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::ITALIC),
)
.percent(app.progress4)
.label(label);
f.render_widget(gauge, chunks[3]);
struct Term {
terminal: Terminal<CrosstermBackend<Stdout>>,
}
impl Term {
pub fn start() -> io::Result<Term> {
stdout().execute(EnterAlternateScreen)?;
enable_raw_mode()?;
let terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
Ok(Self { terminal })
}
pub fn stop() -> io::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
fn draw(&mut self, frame: impl FnOnce(&mut Frame)) -> Result<()> {
self.terminal.draw(frame)?;
Ok(())
}
}

View File

@@ -1,8 +1,9 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/gauge.tape`
Output "target/gauge.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 600
Set Height 550
Hide
Type "cargo run --example=gauge --features=crossterm"
Enter

41
examples/generate.bash Executable file
View File

@@ -0,0 +1,41 @@
#!/bin/bash
# This script is used to generate the images for the examples README
# It requires the following tools:
# - cargo: https://doc.rust-lang.org/cargo/getting-started/installation.html
# - gh: https://github.com/cli/cli
# - git: https://git-scm.com/
# - vhs: https://github.com/charmbracelet/vhs - currently this needs to be installed from the
# main branch, as the latest release doesn't support the theme we use or the Screenshot
# command. Install using `go install github.com/charmbracelet/vhs@main``
# - go: https://golang.org/doc/install
# - ttyd: https://github.com/tsl0922/ttyd
# Exit on error. Append "|| true" if you expect an error.
set -o errexit
# Exit on error inside any functions or subshells.
set -o errtrace
# Do not allow use of undefined vars. Use ${VAR:-} to use an undefined VAR
set -o nounset
# Catch the error in case mysqldump fails (but gzip succeeds) in `mysqldump |gzip`
set -o pipefail
# Turn on traces, useful while debugging but commented out by default
# set -o xtrace
# ensure that running each example doesn't have to wait for the build
cargo build --examples --features=crossterm,all-widgets
for tape in examples/*.tape; do
gif=${tape/examples\//}
gif=${gif/.tape/.gif}
~/go/bin/vhs $tape --quiet
# this can be pasted into the examples README.md
echo "[${gif}]: https://github.com/ratatui-org/ratatui/blob/images/examples/${gif}?raw=true"
done
git switch images
git pull --rebase upstream images
cp target/*.gif examples/
git add examples/*.gif
git commit -m 'docs(examples): update images'
gh pr create
git sw -

View File

@@ -61,7 +61,7 @@ fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
/// Render the application. This is where you would draw the application UI. This example just
/// draws a greeting.
fn render_app(frame: &mut ratatui::Frame<CrosstermBackend<Stdout>>) {
fn render_app(frame: &mut Frame) {
let greeting = Paragraph::new("Hello World! (press 'q' to quit)");
frame.render_widget(greeting, frame.size());
}

View File

@@ -1,6 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/hello_world.tape`
Output "target/hello_world.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 200
Hide

View File

@@ -98,9 +98,7 @@ fn input_handling(tx: mpsc::Sender<Event>) {
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));
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout).unwrap() {
match crossterm::event::read().unwrap() {
crossterm::event::Event::Key(key) => tx.send(Event::Input(key)).unwrap(),
@@ -216,14 +214,14 @@ fn run_app<B: Backend>(
Ok(())
}
fn ui<B: Backend>(f: &mut Frame<B>, downloads: &Downloads) {
fn ui(f: &mut Frame, downloads: &Downloads) {
let size = f.size();
let block = Block::default().title(block::Title::from("Progress").alignment(Alignment::Center));
f.render_widget(block, size);
let chunks = Layout::default()
.constraints(vec![Constraint::Length(2), Constraint::Length(4)])
.constraints([Constraint::Length(2), Constraint::Length(4)])
.margin(1)
.split(size);
@@ -237,7 +235,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, downloads: &Downloads) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Percentage(20), Constraint::Percentage(80)])
.constraints([Constraint::Percentage(20), Constraint::Percentage(80)])
.split(chunks[1]);
// in progress downloads

View File

@@ -1,6 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/inline.tape`
Output "target/inline.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 600
Type "cargo run --example=inline --features=crossterm"

View File

@@ -37,7 +37,7 @@ fn main() -> Result<(), Box<dyn Error>> {
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f))?;
terminal.draw(ui)?;
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
@@ -47,10 +47,10 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
}
}
fn ui<B: Backend>(frame: &mut Frame<B>) {
fn ui(frame: &mut Frame) {
let main_layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
.constraints([
Length(4), // text
Length(50), // examples
Min(0), // fills remaining space
@@ -71,7 +71,7 @@ fn ui<B: Backend>(frame: &mut Frame<B>) {
let example_rows = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
.constraints([
Length(9),
Length(9),
Length(9),
@@ -85,7 +85,7 @@ fn ui<B: Backend>(frame: &mut Frame<B>) {
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![
.constraints([
Length(14),
Length(14),
Length(14),
@@ -169,8 +169,8 @@ fn ui<B: Backend>(frame: &mut Frame<B>) {
}
/// Renders a single example box
fn render_example_combination<B: Backend>(
frame: &mut Frame<B>,
fn render_example_combination(
frame: &mut Frame,
area: Rect,
title: &str,
constraints: Vec<(Constraint, Constraint)>,
@@ -195,11 +195,7 @@ fn render_example_combination<B: Backend>(
}
/// Renders a single example line
fn render_single_example<B: Backend>(
frame: &mut Frame<B>,
area: Rect,
constraints: Vec<Constraint>,
) {
fn render_single_example(frame: &mut Frame, area: Rect, constraints: Vec<Constraint>) {
let red = Paragraph::new(constraint_label(constraints[0])).on_red();
let blue = Paragraph::new(constraint_label(constraints[1])).on_blue();
let green = Paragraph::new("·".repeat(12)).on_green();

View File

@@ -1,7 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/layout.tape`
Output "target/layout.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 1410
Hide

View File

@@ -175,17 +175,15 @@ fn run_app<B: Backend>(
loop {
terminal.draw(|f| ui(f, &mut app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
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(),
KeyCode::Left | KeyCode::Char('h') => app.items.unselect(),
KeyCode::Down | KeyCode::Char('j') => app.items.next(),
KeyCode::Up | KeyCode::Char('k') => app.items.previous(),
_ => {}
}
}
@@ -198,11 +196,11 @@ fn run_app<B: Backend>(
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
fn ui(f: &mut Frame, 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())
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(f.size());
// Iterate through all elements in the `items` app and append some debug text to it.
@@ -275,6 +273,6 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
.collect();
let events_list = List::new(events)
.block(Block::default().borders(Borders::ALL).title("List"))
.start_corner(Corner::BottomLeft);
.direction(ListDirection::BottomToTop);
f.render_widget(events_list, chunks[1]);
}

View File

@@ -1,6 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/list.tape`
Output "target/list.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 600
Hide

View File

@@ -43,10 +43,10 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
}
}
fn ui<B: Backend>(frame: &mut Frame<B>) {
fn ui(frame: &mut Frame) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1), Constraint::Min(0)])
.constraints([Constraint::Length(1), Constraint::Min(0)])
.split(frame.size());
frame.render_widget(
Paragraph::new("Note: not all terminals support all modifiers")
@@ -55,13 +55,13 @@ fn ui<B: Backend>(frame: &mut Frame<B>) {
);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1); 50])
.constraints([Constraint::Length(1); 50])
.split(layout[1])
.iter()
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Percentage(20); 5])
.constraints([Constraint::Percentage(20); 5])
.split(*area)
.to_vec()
})

View File

@@ -1,7 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/modifiers.tape`
Output "target/modifiers.gif"
Set Theme "OceanicMaterial"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 1460
Hide

View File

@@ -102,7 +102,7 @@ fn run_tui<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<
}
/// Render the TUI.
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
fn ui(f: &mut Frame, app: &App) {
let text = vec![
if app.hook_enabled {
Line::from("HOOK IS CURRENTLY **ENABLED**")

View File

@@ -1,6 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/panic.tape`
Output "target/panic.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 600
Type "cargo run --example=panic --features=crossterm"

View File

@@ -64,9 +64,7 @@ fn run_app<B: Backend>(
loop {
terminal.draw(|f| ui(f, &app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
@@ -81,7 +79,7 @@ fn run_app<B: Backend>(
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
fn ui(f: &mut Frame, app: &App) {
let size = f.size();
// Words made "loooong" to demonstrate line breaking.
@@ -94,15 +92,12 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
]
.as_ref(),
)
.constraints([
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
])
.split(size);
let text = vec![

View File

@@ -1,6 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/paragraph.tape`
Output "target/paragraph.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 1800
Hide

View File

@@ -61,11 +61,11 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
fn ui(f: &mut Frame, app: &App) {
let size = f.size();
let chunks = Layout::default()
.constraints([Constraint::Percentage(20), Constraint::Percentage(80)].as_ref())
.constraints([Constraint::Percentage(20), Constraint::Percentage(80)])
.split(size);
let text = if app.show_popup {
@@ -96,25 +96,19 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
]
.as_ref(),
)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
Layout::default()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
]
.as_ref(),
)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
}

View File

@@ -1,6 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/popup.tape`
Output "target/popup.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 600
Hide

71
examples/ratatui-logo.rs Normal file
View File

@@ -0,0 +1,71 @@
use std::{
io::{self, stdout},
thread::sleep,
time::Duration,
};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use indoc::indoc;
use itertools::izip;
use ratatui::{prelude::*, widgets::*};
/// A fun example of using half block characters to draw a logo
fn main() -> io::Result<()> {
let r = indoc! {"
▄▄▄
█▄▄▀
█ █
"}
.lines();
let a = indoc! {"
▄▄
█▄▄█
█ █
"}
.lines();
let t = indoc! {"
▄▄▄
"}
.lines();
let u = indoc! {"
▄ ▄
█ █
▀▄▄▀
"}
.lines();
let i = indoc! {"
"}
.lines();
let mut terminal = init()?;
terminal.draw(|frame| {
let logo = izip!(r, a.clone(), t.clone(), a, t, u, i)
.map(|(r, a, t, a2, t2, u, i)| {
format!("{:5}{:5}{:4}{:5}{:4}{:5}{:5}", r, a, t, a2, t2, u, i)
})
.collect::<Vec<_>>()
.join("\n");
frame.render_widget(Paragraph::new(logo), frame.size());
})?;
sleep(Duration::from_secs(5));
restore()?;
println!();
Ok(())
}
pub fn init() -> io::Result<Terminal<impl Backend>> {
enable_raw_mode()?;
let options = TerminalOptions {
viewport: Viewport::Inline(3),
};
Terminal::with_options(CrosstermBackend::new(stdout()), options)
}
pub fn restore() -> io::Result<()> {
disable_raw_mode()?;
Ok(())
}

View File

@@ -0,0 +1,12 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/popup.tape`
Output "target/ratatui-logo.gif"
Set Theme "Aardvark Blue"
Set Width 550
Set Height 220
Hide
Type "cargo run --example=ratatui-logo --features=crossterm"
Enter
Sleep 2s
Show
Sleep 2s

View File

@@ -57,36 +57,30 @@ fn run_app<B: Backend>(
loop {
terminal.draw(|f| ui(f, &mut app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Char('j') => {
app.vertical_scroll = app.vertical_scroll.saturating_add(1);
app.vertical_scroll_state = app
.vertical_scroll_state
.position(app.vertical_scroll as u16);
app.vertical_scroll_state =
app.vertical_scroll_state.position(app.vertical_scroll);
}
KeyCode::Char('k') => {
app.vertical_scroll = app.vertical_scroll.saturating_sub(1);
app.vertical_scroll_state = app
.vertical_scroll_state
.position(app.vertical_scroll as u16);
app.vertical_scroll_state =
app.vertical_scroll_state.position(app.vertical_scroll);
}
KeyCode::Char('h') => {
app.horizontal_scroll = app.horizontal_scroll.saturating_sub(1);
app.horizontal_scroll_state = app
.horizontal_scroll_state
.position(app.horizontal_scroll as u16);
app.horizontal_scroll_state =
app.horizontal_scroll_state.position(app.horizontal_scroll);
}
KeyCode::Char('l') => {
app.horizontal_scroll = app.horizontal_scroll.saturating_add(1);
app.horizontal_scroll_state = app
.horizontal_scroll_state
.position(app.horizontal_scroll as u16);
app.horizontal_scroll_state =
app.horizontal_scroll_state.position(app.horizontal_scroll);
}
_ => {}
}
@@ -98,7 +92,7 @@ fn run_app<B: Backend>(
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
fn ui(f: &mut Frame, app: &mut App) {
let size = f.size();
// Words made "loooong" to demonstrate line breaking.
@@ -111,16 +105,13 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Min(1),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
]
.as_ref(),
)
.constraints([
Constraint::Min(1),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
])
.split(size);
let text = vec![
@@ -128,7 +119,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
Line::from("This is a line ".red()),
Line::from("This is a line".on_dark_gray()),
Line::from("This is a longer line".crossed_out()),
Line::from(long_line.reset()),
Line::from(long_line.clone()),
Line::from("This is a line".reset()),
Line::from(vec![
Span::raw("Masked text: "),
@@ -141,7 +132,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
Line::from("This is a line ".red()),
Line::from("This is a line".on_dark_gray()),
Line::from("This is a longer line".crossed_out()),
Line::from(long_line.reset()),
Line::from(long_line.clone()),
Line::from("This is a line".reset()),
Line::from(vec![
Span::raw("Masked text: "),
@@ -151,10 +142,8 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
),
]),
];
app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len() as u16);
app.horizontal_scroll_state = app
.horizontal_scroll_state
.content_length(long_line.len() as u16);
app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len());
app.horizontal_scroll_state = app.horizontal_scroll_state.content_length(long_line.len());
let create_block = |title| {
Block::default()

View File

@@ -1,6 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/scrollbar.tape`
Output "target/scrollbar.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 1200
Hide

View File

@@ -109,9 +109,7 @@ fn run_app<B: Backend>(
loop {
terminal.draw(|f| ui(f, &app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
@@ -126,18 +124,14 @@ fn run_app<B: Backend>(
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
fn ui(f: &mut Frame, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(7),
Constraint::Min(0),
]
.as_ref(),
)
.constraints([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Min(0),
])
.split(f.size());
let sparkline = Sparkline::default()
.block(

View File

@@ -1,6 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/sparkline.tape`
Output "target/sparkline.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 600
Hide

View File

@@ -104,8 +104,8 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Down => app.next(),
KeyCode::Up => app.previous(),
KeyCode::Down | KeyCode::Char('j') => app.next(),
KeyCode::Up | KeyCode::Char('k') => app.previous(),
_ => {}
}
}
@@ -113,9 +113,9 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
fn ui(f: &mut Frame, app: &mut App) {
let rects = Layout::default()
.constraints([Constraint::Percentage(100)].as_ref())
.constraints([Constraint::Percentage(100)])
.split(f.size());
let selected_style = Style::default().add_modifier(Modifier::REVERSED);
@@ -127,6 +127,13 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
.style(normal_style)
.height(1)
.bottom_margin(1);
let footer_cells = ["Footer1", "Footer2", "Footer3"]
.iter()
.map(|f| Cell::from(*f).style(Style::default().fg(Color::Yellow)));
let footer = Row::new(footer_cells)
.style(normal_style)
.height(1)
.top_margin(1);
let rows = app.items.iter().map(|item| {
let height = item
.iter()
@@ -137,15 +144,18 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
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(&[
let t = Table::new(
rows,
[
Constraint::Percentage(50),
Constraint::Max(30),
Constraint::Min(10),
]);
],
)
.header(header)
.footer(footer)
.block(Block::default().borders(Borders::ALL).title("Table"))
.highlight_style(selected_style)
.highlight_symbol(">> ");
f.render_stateful_widget(t, rects[0], &mut app.state);
}

View File

@@ -1,6 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/table.tape`
Output "target/table.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 600
Hide

View File

@@ -69,8 +69,8 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Right => app.next(),
KeyCode::Left => app.previous(),
KeyCode::Right | KeyCode::Char('l') => app.next(),
KeyCode::Left | KeyCode::Char('h') => app.previous(),
_ => {}
}
}
@@ -78,11 +78,11 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
fn ui(f: &mut Frame, app: &App) {
let size = f.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
.constraints([Constraint::Length(3), Constraint::Min(0)])
.split(size);
let block = Block::default().on_white().black();
@@ -98,12 +98,8 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
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),
);
.style(Style::default().cyan().on_gray())
.highlight_style(Style::default().bold().on_black());
f.render_widget(tabs, chunks[0]);
let inner = match app.index {
0 => Block::default().title("Inner 0").borders(Borders::ALL),

View File

@@ -1,6 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/tabs.tape`
Output "target/tabs.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 300
Hide

View File

@@ -171,17 +171,14 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
}
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
fn ui(f: &mut Frame, app: &App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(1),
Constraint::Length(3),
Constraint::Min(1),
]
.as_ref(),
)
.constraints([
Constraint::Length(1),
Constraint::Length(3),
Constraint::Min(1),
])
.split(f.size());
let (msg, style) = match app.input_mode {

View File

@@ -1,6 +1,7 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/user_input.tape`
Output "target/user_input.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 600
Hide

View File

@@ -3,3 +3,4 @@ group_imports = "StdExternalCrate"
imports_granularity = "Crate"
wrap_comments = true
comment_width = 100
format_code_in_doc_comments = true

339
src/backend.rs Normal file
View File

@@ -0,0 +1,339 @@
#![warn(missing_docs)]
//! This module provides the backend implementations for different terminal libraries.
//!
//! It defines the [`Backend`] trait which is used to abstract over the specific terminal library
//! being used.
//!
//! Supported terminal backends:
//! - [Crossterm]: enable the `crossterm` feature (enabled by default) and use [`CrosstermBackend`]
//! - [Termion]: enable the `termion` feature and use [`TermionBackend`]
//! - [Termwiz]: enable the `termwiz` feature and use [`TermwizBackend`]
//!
//! Additionally, a [`TestBackend`] is provided for testing purposes.
//!
//! See the [Backend Comparison] section of the [Ratatui Website] for more details on the different
//! backends.
//!
//! Each backend supports a number of features, such as [raw mode](#raw-mode), [alternate
//! screen](#alternate-screen), and [mouse capture](#mouse-capture). These features are generally
//! not enabled by default, and must be enabled by the application before they can be used. See the
//! documentation for each backend for more details.
//!
//! Note: most applications should use the [`Terminal`] struct instead of directly calling methods
//! on the backend.
//!
//! # Example
//!
//! ```rust,no_run
//! use std::io::stdout;
//!
//! use ratatui::prelude::*;
//!
//! let backend = CrosstermBackend::new(stdout());
//! let mut terminal = Terminal::new(backend)?;
//! terminal.clear()?;
//! terminal.draw(|frame| {
//! // -- snip --
//! })?;
//! # std::io::Result::Ok(())
//! ```
//!
//! See the the [examples] directory for more examples.
//!
//! # Raw Mode
//!
//! Raw mode is a mode where the terminal does not perform any processing or handling of the input
//! and output. This means that features such as echoing input characters, line buffering, and
//! special character processing (e.g., CTRL-C for SIGINT) are disabled. This is useful for
//! applications that want to have complete control over the terminal input and output, processing
//! each keystroke themselves.
//!
//! For example, in raw mode, the terminal will not perform line buffering on the input, so the
//! application will receive each key press as it is typed, instead of waiting for the user to
//! press enter. This makes it suitable for real-time applications like text editors,
//! terminal-based games, and more.
//!
//! Each backend handles raw mode differently, so the behavior may vary depending on the backend
//! being used. Be sure to consult the backend's specific documentation for exact details on how it
//! implements raw mode.
//! # Alternate Screen
//!
//! The alternate screen is a separate buffer that some terminals provide, distinct from the main
//! screen. When activated, the terminal will display the alternate screen, hiding the current
//! content of the main screen. Applications can write to this screen as if it were the regular
//! terminal display, but when the application exits, the terminal will switch back to the main
//! screen, and the contents of the alternate screen will be cleared. This is useful for
//! applications like text editors or terminal games that want to use the full terminal window
//! without disrupting the command line or other terminal content.
//!
//! This creates a seamless transition between the application and the regular terminal session, as
//! the content displayed before launching the application will reappear after the application
//! exits.
//!
//! Note that not all terminal emulators support the alternate screen, and even those that do may
//! handle it differently. As a result, the behavior may vary depending on the backend being used.
//! Always consult the specific backend's documentation to understand how it implements the
//! alternate screen.
//!
//! # Mouse Capture
//!
//! Mouse capture is a mode where the terminal captures mouse events such as clicks, scrolls, and
//! movement, and sends them to the application as special sequences or events. This enables the
//! application to handle and respond to mouse actions, providing a more interactive and graphical
//! user experience within the terminal. It's particularly useful for applications like
//! terminal-based games, text editors, or other programs that require more direct interaction from
//! the user.
//!
//! Each backend handles mouse capture differently, with variations in the types of events that can
//! be captured and how they are represented. As such, the behavior may vary depending on the
//! backend being used, and developers should consult the specific backend's documentation to
//! understand how it implements mouse capture.
//!
//! [`TermionBackend`]: termion/struct.TermionBackend.html
//! [`Terminal`]: crate::terminal::Terminal
//! [`TermionBackend`]: termion/struct.TermionBackend.html
//! [Crossterm]: https://crates.io/crates/crossterm
//! [Termion]: https://crates.io/crates/termion
//! [Termwiz]: https://crates.io/crates/termwiz
//! [examples]: https://github.com/ratatui-org/ratatui/tree/main/examples#readme
//! [Backend Comparison]:
//! https://ratatui.rs/concepts/backends/comparison/
//! [Ratatui Website]: https://ratatui-org.github.io/ratatui-book
use std::io;
use strum::{Display, EnumString};
use crate::{buffer::Cell, layout::Size, prelude::Rect};
#[cfg(feature = "termion")]
mod termion;
#[cfg(feature = "termion")]
pub use self::termion::TermionBackend;
#[cfg(feature = "crossterm")]
mod crossterm;
#[cfg(feature = "crossterm")]
pub use self::crossterm::CrosstermBackend;
#[cfg(feature = "termwiz")]
mod termwiz;
#[cfg(feature = "termwiz")]
pub use self::termwiz::TermwizBackend;
mod test;
pub use self::test::TestBackend;
/// Enum representing the different types of clearing operations that can be performed
/// on the terminal screen.
#[derive(Debug, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum ClearType {
/// Clear the entire screen.
All,
/// Clear everything after the cursor.
AfterCursor,
/// Clear everything before the cursor.
BeforeCursor,
/// Clear the current line.
CurrentLine,
/// Clear everything from the cursor until the next newline.
UntilNewLine,
}
/// The window size in characters (columns / rows) as well as pixels.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct WindowSize {
/// Size of the window in characters (columns / rows).
pub columns_rows: Size,
/// Size of the window in pixels.
///
/// The `pixels` fields may not be implemented by all terminals and return `0,0`. See
/// <https://man7.org/linux/man-pages/man4/tty_ioctl.4.html> under section "Get and set window
/// size" / TIOCGWINSZ where the fields are commented as "unused".
pub pixels: Size,
}
/// The `Backend` trait provides an abstraction over different terminal libraries. It defines the
/// methods required to draw content, manipulate the cursor, and clear the terminal screen.
///
/// Most applications should not need to interact with the `Backend` trait directly as the
/// [`Terminal`] struct provides a higher level interface for interacting with the terminal.
///
/// [`Terminal`]: crate::terminal::Terminal
pub trait Backend {
/// Draw the given content to the terminal screen.
///
/// The content is provided as an iterator over `(u16, u16, &Cell)` tuples, where the first two
/// elements represent the x and y coordinates, and the third element is a reference to the
/// [`Cell`] to be drawn.
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>;
/// Insert `n` line breaks to the terminal screen.
///
/// This method is optional and may not be implemented by all backends.
fn append_lines(&mut self, _n: u16) -> io::Result<()> {
Ok(())
}
/// Hide the cursor on the terminal screen.
///
///
/// See also [`show_cursor`].
/// # Example
///
/// ```rust
/// # use ratatui::backend::{Backend, TestBackend};
/// # let mut backend = TestBackend::new(80, 25);
/// backend.hide_cursor()?;
/// // do something with hidden cursor
/// backend.show_cursor()?;
/// # std::io::Result::Ok(())
/// ```
///
/// [`show_cursor`]: Backend::show_cursor
fn hide_cursor(&mut self) -> io::Result<()>;
/// Show the cursor on the terminal screen.
///
/// See [`hide_cursor`] for an example.
///
/// [`hide_cursor`]: Backend::hide_cursor
fn show_cursor(&mut self) -> io::Result<()>;
/// Get the current cursor position on the terminal screen.
///
/// The returned tuple contains the x and y coordinates of the cursor. The origin
/// (0, 0) is at the top left corner of the screen.
///
/// See [`set_cursor`] for an example.
///
/// [`set_cursor`]: Backend::set_cursor
fn get_cursor(&mut self) -> io::Result<(u16, u16)>;
/// Set the cursor position on the terminal screen to the given x and y coordinates.
///
/// The origin (0, 0) is at the top left corner of the screen.
///
/// # Example
///
/// ```rust
/// # use ratatui::backend::{Backend, TestBackend};
/// # let mut backend = TestBackend::new(80, 25);
/// backend.set_cursor(10, 20)?;
/// assert_eq!(backend.get_cursor()?, (10, 20));
/// # std::io::Result::Ok(())
/// ```
///
/// [`get_cursor`]: Backend::get_cursor
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()>;
/// Clears the whole terminal screen
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::{Backend, TestBackend};
/// # let mut backend = TestBackend::new(80, 25);
/// backend.clear()?;
/// # std::io::Result::Ok(())
/// ```
fn clear(&mut self) -> io::Result<()>;
/// Clears a specific region of the terminal specified by the [`ClearType`] parameter
///
/// This method is optional and may not be implemented by all backends. The default
/// implementation calls [`clear`] if the `clear_type` is [`ClearType::All`] and returns an
/// error otherwise.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::{prelude::*, backend::{TestBackend, ClearType}};
/// # let mut backend = TestBackend::new(80, 25);
/// backend.clear_region(ClearType::All)?;
/// # std::io::Result::Ok(())
/// ```
///
/// # Errors
///
/// This method will return an error if the terminal screen could not be cleared. It will also
/// return an error if the `clear_type` is not supported by the backend.
///
/// [`clear`]: Backend::clear
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
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"),
)),
}
}
/// Get the size of the terminal screen in columns/rows as a [`Rect`].
///
/// The returned [`Rect`] contains the width and height of the terminal screen.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::{prelude::*, backend::TestBackend};
/// let backend = TestBackend::new(80, 25);
/// assert_eq!(backend.size()?, Rect::new(0, 0, 80, 25));
/// # std::io::Result::Ok(())
/// ```
fn size(&self) -> io::Result<Rect>;
/// Get the size of the terminal screen in columns/rows and pixels as a [`WindowSize`].
///
/// The reason for this not returning only the pixel size, given the redundancy with the
/// `size()` method, is that the underlying backends most likely get both values with one
/// syscall, and the user is also most likely to need columns and rows along with pixel size.
fn window_size(&mut self) -> io::Result<WindowSize>;
/// Flush any buffered content to the terminal screen.
fn flush(&mut self) -> io::Result<()>;
}
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::*;
#[test]
fn clear_type_tostring() {
assert_eq!(ClearType::All.to_string(), "All");
assert_eq!(ClearType::AfterCursor.to_string(), "AfterCursor");
assert_eq!(ClearType::BeforeCursor.to_string(), "BeforeCursor");
assert_eq!(ClearType::CurrentLine.to_string(), "CurrentLine");
assert_eq!(ClearType::UntilNewLine.to_string(), "UntilNewLine");
}
#[test]
fn clear_type_from_str() {
assert_eq!("All".parse::<ClearType>(), Ok(ClearType::All));
assert_eq!(
"AfterCursor".parse::<ClearType>(),
Ok(ClearType::AfterCursor)
);
assert_eq!(
"BeforeCursor".parse::<ClearType>(),
Ok(ClearType::BeforeCursor)
);
assert_eq!(
"CurrentLine".parse::<ClearType>(),
Ok(ClearType::CurrentLine)
);
assert_eq!(
"UntilNewLine".parse::<ClearType>(),
Ok(ClearType::UntilNewLine)
);
assert_eq!("".parse::<ClearType>(), Err(ParseError::VariantNotFound));
}
}

View File

@@ -1,59 +1,104 @@
//! This module provides the `CrosstermBackend` implementation for the `Backend` trait.
//! It uses the `crossterm` crate to interact with the terminal.
//! This module provides the [`CrosstermBackend`] implementation for the [`Backend`] trait. It uses
//! the [Crossterm] crate to interact with the terminal.
//!
//!
//! [`Backend`]: trait.Backend.html
//! [`CrosstermBackend`]: struct.CrosstermBackend.html
//! [Crossterm]: https://crates.io/crates/crossterm
use std::io::{self, Write};
#[cfg(feature = "underline-color")]
use crossterm::style::SetUnderlineColor;
use crossterm::{
cursor::{Hide, MoveTo, Show},
execute, queue,
style::{
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
SetForegroundColor, SetUnderlineColor,
Attribute as CAttribute, Attributes as CAttributes, Color as CColor, ContentStyle, Print,
SetAttribute, SetBackgroundColor, SetForegroundColor,
},
terminal::{self, Clear},
};
use crate::{
backend::{Backend, ClearType},
backend::{Backend, ClearType, WindowSize},
buffer::Cell,
layout::Rect,
style::{Color, Modifier},
layout::Size,
prelude::Rect,
style::{Color, Modifier, Style},
};
/// A backend implementation using the `crossterm` crate.
/// A [`Backend`] implementation that uses [Crossterm] to render to the terminal.
///
/// The `CrosstermBackend` struct is a wrapper around a type implementing `Write`, which
/// is used to send commands to the terminal. It provides methods for drawing content,
/// manipulating the cursor, and clearing the terminal screen.
/// The `CrosstermBackend` struct is a wrapper around a writer implementing [`Write`], which is
/// used to send commands to the terminal. It provides methods for drawing content, manipulating
/// the cursor, and clearing the terminal screen.
///
/// Most applications should not call the methods on `CrosstermBackend` directly, but will instead
/// use the [`Terminal`] struct, which provides a more ergonomic interface.
///
/// Usually applications will enable raw mode and switch to alternate screen mode after creating
/// a `CrosstermBackend`. This is done by calling [`crossterm::terminal::enable_raw_mode`] and
/// [`crossterm::terminal::EnterAlternateScreen`] (and the corresponding disable/leave functions
/// when the application exits). This is not done automatically by the backend because it is
/// possible that the application may want to use the terminal for other purposes (like showing
/// help text) before entering alternate screen mode.
///
/// # Example
///
/// ```rust
/// use ratatui::backend::{Backend, CrosstermBackend};
/// ```rust,no_run
/// use std::io::{stderr, stdout};
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let buffer = std::io::stdout();
/// let mut backend = CrosstermBackend::new(buffer);
/// backend.clear()?;
/// # Ok(())
/// # }
/// use crossterm::{
/// terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
/// ExecutableCommand,
/// };
/// use ratatui::prelude::*;
///
/// let mut backend = CrosstermBackend::new(stdout());
/// // or
/// let backend = CrosstermBackend::new(stderr());
/// let mut terminal = Terminal::new(backend)?;
///
/// enable_raw_mode()?;
/// stdout().execute(EnterAlternateScreen)?;
///
/// terminal.clear()?;
/// terminal.draw(|frame| {
/// // -- snip --
/// })?;
///
/// stdout().execute(LeaveAlternateScreen)?;
/// disable_raw_mode()?;
///
/// # std::io::Result::Ok(())
/// ```
///
/// See the the [examples] directory for more examples. See the [`backend`] module documentation
/// for more details on raw mode and alternate screen.
///
/// [`Write`]: std::io::Write
/// [`Terminal`]: crate::terminal::Terminal
/// [`backend`]: crate::backend
/// [Crossterm]: https://crates.io/crates/crossterm
/// [examples]: https://github.com/ratatui-org/ratatui/tree/main/examples#examples
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct CrosstermBackend<W: Write> {
buffer: W,
/// The writer used to send commands to the terminal.
writer: W,
}
impl<W> CrosstermBackend<W>
where
W: Write,
{
/// Creates a new `CrosstermBackend` with the given buffer.
pub fn new(buffer: W) -> CrosstermBackend<W> {
CrosstermBackend { buffer }
/// Creates a new `CrosstermBackend` with the given writer.
///
/// # Example
///
/// ```rust,no_run
/// # use std::io::stdout;
/// # use ratatui::prelude::*;
/// let backend = CrosstermBackend::new(stdout());
/// ```
pub fn new(writer: W) -> CrosstermBackend<W> {
CrosstermBackend { writer }
}
}
@@ -63,12 +108,12 @@ where
{
/// Writes a buffer of bytes to the underlying buffer.
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.buffer.write(buf)
self.writer.write(buf)
}
/// Flushes the underlying buffer.
fn flush(&mut self) -> io::Result<()> {
self.buffer.flush()
self.writer.flush()
}
}
@@ -82,13 +127,14 @@ where
{
let mut fg = Color::Reset;
let mut bg = Color::Reset;
#[cfg(feature = "underline-color")]
let mut underline_color = Color::Reset;
let mut modifier = Modifier::empty();
let mut last_pos: Option<(u16, u16)> = None;
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) {
queue!(self.buffer, MoveTo(x, y))?;
queue!(self.writer, MoveTo(x, y))?;
}
last_pos = Some((x, y));
if cell.modifier != modifier {
@@ -96,43 +142,52 @@ where
from: modifier,
to: cell.modifier,
};
diff.queue(&mut self.buffer)?;
diff.queue(&mut self.writer)?;
modifier = cell.modifier;
}
if cell.fg != fg {
let color = CColor::from(cell.fg);
queue!(self.buffer, SetForegroundColor(color))?;
queue!(self.writer, SetForegroundColor(color))?;
fg = cell.fg;
}
if cell.bg != bg {
let color = CColor::from(cell.bg);
queue!(self.buffer, SetBackgroundColor(color))?;
queue!(self.writer, SetBackgroundColor(color))?;
bg = cell.bg;
}
#[cfg(feature = "underline-color")]
if cell.underline_color != underline_color {
let color = CColor::from(cell.underline_color);
queue!(self.buffer, SetUnderlineColor(color))?;
queue!(self.writer, SetUnderlineColor(color))?;
underline_color = cell.underline_color;
}
queue!(self.buffer, Print(&cell.symbol))?;
queue!(self.writer, Print(cell.symbol()))?;
}
queue!(
self.buffer,
#[cfg(feature = "underline-color")]
return queue!(
self.writer,
SetForegroundColor(CColor::Reset),
SetBackgroundColor(CColor::Reset),
SetUnderlineColor(CColor::Reset),
SetAttribute(CAttribute::Reset)
)
SetAttribute(CAttribute::Reset),
);
#[cfg(not(feature = "underline-color"))]
return queue!(
self.writer,
SetForegroundColor(CColor::Reset),
SetBackgroundColor(CColor::Reset),
SetAttribute(CAttribute::Reset),
);
}
fn hide_cursor(&mut self) -> io::Result<()> {
execute!(self.buffer, Hide)
execute!(self.writer, Hide)
}
fn show_cursor(&mut self) -> io::Result<()> {
execute!(self.buffer, Show)
execute!(self.writer, Show)
}
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
@@ -141,7 +196,7 @@ where
}
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
execute!(self.buffer, MoveTo(x, y))
execute!(self.writer, MoveTo(x, y))
}
fn clear(&mut self) -> io::Result<()> {
@@ -150,7 +205,7 @@ where
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
execute!(
self.buffer,
self.writer,
Clear(match clear_type {
ClearType::All => crossterm::terminal::ClearType::All,
ClearType::AfterCursor => crossterm::terminal::ClearType::FromCursorDown,
@@ -163,20 +218,34 @@ where
fn append_lines(&mut self, n: u16) -> io::Result<()> {
for _ in 0..n {
queue!(self.buffer, Print("\n"))?;
queue!(self.writer, Print("\n"))?;
}
self.buffer.flush()
self.writer.flush()
}
fn size(&self) -> io::Result<Rect> {
let (width, height) =
terminal::size().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
let (width, height) = terminal::size()?;
Ok(Rect::new(0, 0, width, height))
}
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
let crossterm::terminal::WindowSize {
columns,
rows,
width,
height,
} = terminal::window_size()?;
Ok(WindowSize {
columns_rows: Size {
width: columns,
height: rows,
},
pixels: Size { width, height },
})
}
fn flush(&mut self) -> io::Result<()> {
self.buffer.flush()
self.writer.flush()
}
}
@@ -206,6 +275,32 @@ impl From<Color> for CColor {
}
}
impl From<CColor> for Color {
fn from(value: CColor) -> Self {
match value {
CColor::Reset => Self::Reset,
CColor::Black => Self::Black,
CColor::DarkRed => Self::Red,
CColor::DarkGreen => Self::Green,
CColor::DarkYellow => Self::Yellow,
CColor::DarkBlue => Self::Blue,
CColor::DarkMagenta => Self::Magenta,
CColor::DarkCyan => Self::Cyan,
CColor::Grey => Self::Gray,
CColor::DarkGrey => Self::DarkGray,
CColor::Red => Self::LightRed,
CColor::Green => Self::LightGreen,
CColor::Blue => Self::LightBlue,
CColor::Yellow => Self::LightYellow,
CColor::Magenta => Self::LightMagenta,
CColor::Cyan => Self::LightCyan,
CColor::White => Self::White,
CColor::Rgb { r, g, b } => Self::Rgb(r, g, b),
CColor::AnsiValue(v) => Self::Indexed(v),
}
}
}
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
/// values. This is useful when updating the terminal display, as it allows for more
/// efficient updates by only sending the necessary changes.
@@ -276,3 +371,303 @@ impl ModifierDiff {
Ok(())
}
}
impl From<CAttribute> for Modifier {
fn from(value: CAttribute) -> Self {
// `Attribute*s*` (note the *s*) contains multiple `Attribute`
// We convert `Attribute` to `Attribute*s*` (containing only 1 value) to avoid implementing
// the conversion again
Modifier::from(CAttributes::from(value))
}
}
impl From<CAttributes> for Modifier {
fn from(value: CAttributes) -> Self {
let mut res = Modifier::empty();
if value.has(CAttribute::Bold) {
res |= Modifier::BOLD;
}
if value.has(CAttribute::Dim) {
res |= Modifier::DIM;
}
if value.has(CAttribute::Italic) {
res |= Modifier::ITALIC;
}
if value.has(CAttribute::Underlined)
|| value.has(CAttribute::DoubleUnderlined)
|| value.has(CAttribute::Undercurled)
|| value.has(CAttribute::Underdotted)
|| value.has(CAttribute::Underdashed)
{
res |= Modifier::UNDERLINED;
}
if value.has(CAttribute::SlowBlink) {
res |= Modifier::SLOW_BLINK;
}
if value.has(CAttribute::RapidBlink) {
res |= Modifier::RAPID_BLINK;
}
if value.has(CAttribute::Reverse) {
res |= Modifier::REVERSED;
}
if value.has(CAttribute::Hidden) {
res |= Modifier::HIDDEN;
}
if value.has(CAttribute::CrossedOut) {
res |= Modifier::CROSSED_OUT;
}
res
}
}
impl From<ContentStyle> for Style {
fn from(value: ContentStyle) -> Self {
let mut sub_modifier = Modifier::empty();
if value.attributes.has(CAttribute::NoBold) {
sub_modifier |= Modifier::BOLD;
}
if value.attributes.has(CAttribute::NoItalic) {
sub_modifier |= Modifier::ITALIC;
}
if value.attributes.has(CAttribute::NotCrossedOut) {
sub_modifier |= Modifier::CROSSED_OUT;
}
if value.attributes.has(CAttribute::NoUnderline) {
sub_modifier |= Modifier::UNDERLINED;
}
if value.attributes.has(CAttribute::NoHidden) {
sub_modifier |= Modifier::HIDDEN;
}
if value.attributes.has(CAttribute::NoBlink) {
sub_modifier |= Modifier::RAPID_BLINK | Modifier::SLOW_BLINK;
}
if value.attributes.has(CAttribute::NoReverse) {
sub_modifier |= Modifier::REVERSED;
}
Self {
fg: value.foreground_color.map(|c| c.into()),
bg: value.background_color.map(|c| c.into()),
#[cfg(feature = "underline-color")]
underline_color: value.underline_color.map(|c| c.into()),
add_modifier: value.attributes.into(),
sub_modifier,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_crossterm_color() {
assert_eq!(Color::from(CColor::Reset), Color::Reset);
assert_eq!(Color::from(CColor::Black), Color::Black);
assert_eq!(Color::from(CColor::DarkGrey), Color::DarkGray);
assert_eq!(Color::from(CColor::Red), Color::LightRed);
assert_eq!(Color::from(CColor::DarkRed), Color::Red);
assert_eq!(Color::from(CColor::Green), Color::LightGreen);
assert_eq!(Color::from(CColor::DarkGreen), Color::Green);
assert_eq!(Color::from(CColor::Yellow), Color::LightYellow);
assert_eq!(Color::from(CColor::DarkYellow), Color::Yellow);
assert_eq!(Color::from(CColor::Blue), Color::LightBlue);
assert_eq!(Color::from(CColor::DarkBlue), Color::Blue);
assert_eq!(Color::from(CColor::Magenta), Color::LightMagenta);
assert_eq!(Color::from(CColor::DarkMagenta), Color::Magenta);
assert_eq!(Color::from(CColor::Cyan), Color::LightCyan);
assert_eq!(Color::from(CColor::DarkCyan), Color::Cyan);
assert_eq!(Color::from(CColor::White), Color::White);
assert_eq!(Color::from(CColor::Grey), Color::Gray);
assert_eq!(
Color::from(CColor::Rgb { r: 0, g: 0, b: 0 }),
Color::Rgb(0, 0, 0)
);
assert_eq!(
Color::from(CColor::Rgb {
r: 10,
g: 20,
b: 30
}),
Color::Rgb(10, 20, 30)
);
assert_eq!(Color::from(CColor::AnsiValue(32)), Color::Indexed(32));
assert_eq!(Color::from(CColor::AnsiValue(37)), Color::Indexed(37));
}
mod modifier {
use super::*;
#[test]
fn from_crossterm_attribute() {
assert_eq!(Modifier::from(CAttribute::Reset), Modifier::empty());
assert_eq!(Modifier::from(CAttribute::Bold), Modifier::BOLD);
assert_eq!(Modifier::from(CAttribute::Italic), Modifier::ITALIC);
assert_eq!(Modifier::from(CAttribute::Underlined), Modifier::UNDERLINED);
assert_eq!(
Modifier::from(CAttribute::DoubleUnderlined),
Modifier::UNDERLINED
);
assert_eq!(
Modifier::from(CAttribute::Underdotted),
Modifier::UNDERLINED
);
assert_eq!(Modifier::from(CAttribute::Dim), Modifier::DIM);
assert_eq!(
Modifier::from(CAttribute::NormalIntensity),
Modifier::empty()
);
assert_eq!(
Modifier::from(CAttribute::CrossedOut),
Modifier::CROSSED_OUT
);
assert_eq!(Modifier::from(CAttribute::NoUnderline), Modifier::empty());
assert_eq!(Modifier::from(CAttribute::OverLined), Modifier::empty());
assert_eq!(Modifier::from(CAttribute::SlowBlink), Modifier::SLOW_BLINK);
assert_eq!(
Modifier::from(CAttribute::RapidBlink),
Modifier::RAPID_BLINK
);
assert_eq!(Modifier::from(CAttribute::Hidden), Modifier::HIDDEN);
assert_eq!(Modifier::from(CAttribute::NoHidden), Modifier::empty());
assert_eq!(Modifier::from(CAttribute::Reverse), Modifier::REVERSED);
}
#[test]
fn from_crossterm_attributes() {
assert_eq!(
Modifier::from(CAttributes::from(CAttribute::Bold)),
Modifier::BOLD
);
assert_eq!(
Modifier::from(CAttributes::from(
[CAttribute::Bold, CAttribute::Italic].as_ref()
)),
Modifier::BOLD | Modifier::ITALIC
);
assert_eq!(
Modifier::from(CAttributes::from(
[CAttribute::Bold, CAttribute::NotCrossedOut].as_ref()
)),
Modifier::BOLD
);
assert_eq!(
Modifier::from(CAttributes::from(
[CAttribute::Dim, CAttribute::Underdotted].as_ref()
)),
Modifier::DIM | Modifier::UNDERLINED
);
assert_eq!(
Modifier::from(CAttributes::from(
[CAttribute::Dim, CAttribute::SlowBlink, CAttribute::Italic].as_ref()
)),
Modifier::DIM | Modifier::SLOW_BLINK | Modifier::ITALIC
);
assert_eq!(
Modifier::from(CAttributes::from(
[
CAttribute::Hidden,
CAttribute::NoUnderline,
CAttribute::NotCrossedOut
]
.as_ref()
)),
Modifier::HIDDEN
);
assert_eq!(
Modifier::from(CAttributes::from(CAttribute::Reverse)),
Modifier::REVERSED
);
assert_eq!(
Modifier::from(CAttributes::from(CAttribute::Reset)),
Modifier::empty()
);
assert_eq!(
Modifier::from(CAttributes::from(
[CAttribute::RapidBlink, CAttribute::CrossedOut].as_ref()
)),
Modifier::RAPID_BLINK | Modifier::CROSSED_OUT
);
}
}
#[test]
fn from_crossterm_content_style() {
assert_eq!(Style::from(ContentStyle::default()), Style::default());
assert_eq!(
Style::from(ContentStyle {
foreground_color: Some(CColor::DarkYellow),
..Default::default()
}),
Style::default().fg(Color::Yellow)
);
assert_eq!(
Style::from(ContentStyle {
background_color: Some(CColor::DarkYellow),
..Default::default()
}),
Style::default().bg(Color::Yellow)
);
assert_eq!(
Style::from(ContentStyle {
attributes: CAttributes::from(CAttribute::Bold),
..Default::default()
}),
Style::default().add_modifier(Modifier::BOLD)
);
assert_eq!(
Style::from(ContentStyle {
attributes: CAttributes::from(CAttribute::NoBold),
..Default::default()
}),
Style::default().remove_modifier(Modifier::BOLD)
);
assert_eq!(
Style::from(ContentStyle {
attributes: CAttributes::from(CAttribute::Italic),
..Default::default()
}),
Style::default().add_modifier(Modifier::ITALIC)
);
assert_eq!(
Style::from(ContentStyle {
attributes: CAttributes::from(CAttribute::NoItalic),
..Default::default()
}),
Style::default().remove_modifier(Modifier::ITALIC)
);
assert_eq!(
Style::from(ContentStyle {
attributes: CAttributes::from([CAttribute::Bold, CAttribute::Italic].as_ref()),
..Default::default()
}),
Style::default()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::ITALIC)
);
assert_eq!(
Style::from(ContentStyle {
attributes: CAttributes::from([CAttribute::NoBold, CAttribute::NoItalic].as_ref()),
..Default::default()
}),
Style::default()
.remove_modifier(Modifier::BOLD)
.remove_modifier(Modifier::ITALIC)
);
}
#[test]
#[cfg(feature = "underline-color")]
fn from_crossterm_content_style_underline() {
assert_eq!(
Style::from(ContentStyle {
underline_color: Some(CColor::DarkRed),
..Default::default()
}),
Style::default().underline_color(Color::Red)
)
}
}

View File

@@ -1,157 +0,0 @@
//! This module provides the backend implementations for different terminal libraries.
//! It defines the [`Backend`] trait which is used to abstract over the specific
//! terminal library being used.
//!
//! The following terminal libraries are supported:
//! - Crossterm (with the `crossterm` feature)
//! - Termion (with the `termion` feature)
//! - Termwiz (with the `termwiz` feature)
//!
//! Additionally, a [`TestBackend`] is provided for testing purposes.
//!
//! # Example
//!
//! ```rust
//! use ratatui::backend::{Backend, CrosstermBackend};
//!
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let buffer = std::io::stdout();
//! let mut backend = CrosstermBackend::new(buffer);
//! backend.clear()?;
//! # Ok(())
//! # }
//! ```
//!
//! [`Backend`]: trait.Backend.html
//! [`TestBackend`]: struct.TestBackend.html
use std::io;
use strum::{Display, EnumString};
use crate::{buffer::Cell, layout::Rect};
#[cfg(feature = "termion")]
mod termion;
#[cfg(feature = "termion")]
pub use self::termion::TermionBackend;
#[cfg(feature = "crossterm")]
mod crossterm;
#[cfg(feature = "crossterm")]
pub use self::crossterm::CrosstermBackend;
#[cfg(feature = "termwiz")]
mod termwiz;
#[cfg(feature = "termwiz")]
pub use self::termwiz::TermwizBackend;
mod test;
pub use self::test::TestBackend;
/// Enum representing the different types of clearing operations that can be performed
/// on the terminal screen.
#[derive(Debug, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum ClearType {
All,
AfterCursor,
BeforeCursor,
CurrentLine,
UntilNewLine,
}
/// The `Backend` trait provides an abstraction over different terminal libraries.
/// It defines the methods required to draw content, manipulate the cursor, and
/// clear the terminal screen.
pub trait Backend {
/// Draw the given content to the terminal screen.
///
/// The content is provided as an iterator over `(u16, u16, &Cell)` tuples,
/// where the first two elements represent the x and y coordinates, and the
/// third element is a reference to the [`Cell`] to be drawn.
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.
///
/// This method is optional and may not be implemented by all backends.
fn append_lines(&mut self, _n: u16) -> io::Result<()> {
Ok(())
}
/// Hide the cursor on the terminal screen.
fn hide_cursor(&mut self) -> Result<(), io::Error>;
/// Show the cursor on the terminal screen.
fn show_cursor(&mut self) -> Result<(), io::Error>;
/// Get the current cursor position on the terminal screen.
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error>;
/// Set the cursor position on the terminal screen to the given x and y coordinates.
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
///
/// This method is optional and may not be implemented by all backends.
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"),
)),
}
}
/// Get the size of the terminal screen as a [`Rect`].
fn size(&self) -> Result<Rect, io::Error>;
/// Flush any buffered content to the terminal screen.
fn flush(&mut self) -> Result<(), io::Error>;
}
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::*;
#[test]
fn clear_type_tostring() {
assert_eq!(ClearType::All.to_string(), "All");
assert_eq!(ClearType::AfterCursor.to_string(), "AfterCursor");
assert_eq!(ClearType::BeforeCursor.to_string(), "BeforeCursor");
assert_eq!(ClearType::CurrentLine.to_string(), "CurrentLine");
assert_eq!(ClearType::UntilNewLine.to_string(), "UntilNewLine");
}
#[test]
fn clear_type_from_str() {
assert_eq!("All".parse::<ClearType>(), Ok(ClearType::All));
assert_eq!(
"AfterCursor".parse::<ClearType>(),
Ok(ClearType::AfterCursor)
);
assert_eq!(
"BeforeCursor".parse::<ClearType>(),
Ok(ClearType::BeforeCursor)
);
assert_eq!(
"CurrentLine".parse::<ClearType>(),
Ok(ClearType::CurrentLine)
);
assert_eq!(
"UntilNewLine".parse::<ClearType>(),
Ok(ClearType::UntilNewLine)
);
assert_eq!("".parse::<ClearType>(), Err(ParseError::VariantNotFound));
}
}

View File

@@ -1,51 +1,89 @@
//! This module provides the `TermionBackend` implementation for the [`Backend`] trait.
//! It uses the Termion crate to interact with the terminal.
//! This module provides the [`TermionBackend`] implementation for the [`Backend`] trait. It uses
//! the [Termion] crate to interact with the terminal.
//!
//! [`Backend`]: crate::backend::Backend
//! [`TermionBackend`]: crate::backend::TermionBackend
//! [Termion]: https://docs.rs/termion
use std::{
fmt,
io::{self, Write},
};
use termion::{color as tcolor, style as tstyle};
use crate::{
backend::{Backend, ClearType},
backend::{Backend, ClearType, WindowSize},
buffer::Cell,
layout::Rect,
style::{Color, Modifier},
prelude::Rect,
style::{Color, Modifier, Style},
};
/// A backend that uses the Termion library to draw content, manipulate the cursor,
/// and clear the terminal screen.
/// A [`Backend`] implementation that uses [Termion] to render to the terminal.
///
/// The `TermionBackend` struct is a wrapper around a writer implementing [`Write`], which is used
/// to send commands to the terminal. It provides methods for drawing content, manipulating the
/// cursor, and clearing the terminal screen.
///
/// Most applications should not call the methods on `TermionBackend` directly, but will instead
/// use the [`Terminal`] struct, which provides a more ergonomic interface.
///
/// Usually applications will enable raw mode and switch to alternate screen mode when starting.
/// This is done by calling [`IntoRawMode::into_raw_mode()`] and
/// [`IntoAlternateScreen::into_alternate_screen()`] on the writer before creating the backend.
/// This is not done automatically by the backend because it is possible that the application may
/// want to use the terminal for other purposes (like showing help text) before entering alternate
/// screen mode. This backend automatically disable raw mode and switches back to the primary
/// screen when the writer is dropped.
///
/// # Example
///
/// ```rust
/// use ratatui::backend::{Backend, TermionBackend};
/// ```rust,no_run
/// use std::io::{stderr, stdout};
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let stdout = std::io::stdout();
/// let mut backend = TermionBackend::new(stdout);
/// backend.clear()?;
/// # Ok(())
/// # }
/// use ratatui::prelude::*;
/// use termion::{raw::IntoRawMode, screen::IntoAlternateScreen};
///
/// let writer = stdout().into_raw_mode()?.into_alternate_screen()?;
/// let mut backend = TermionBackend::new(writer);
/// // or
/// let writer = stderr().into_raw_mode()?.into_alternate_screen()?;
/// let backend = TermionBackend::new(stderr());
/// let mut terminal = Terminal::new(backend)?;
///
/// terminal.clear()?;
/// terminal.draw(|frame| {
/// // -- snip --
/// })?;
/// # std::io::Result::Ok(())
/// ```
///
/// [`IntoRawMode::into_raw_mode()`]: termion::raw::IntoRawMode
/// [`IntoAlternateScreen::into_alternate_screen()`]: termion::screen::IntoAlternateScreen
/// [`Terminal`]: crate::terminal::Terminal
/// [Termion]: https://docs.rs/termion
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct TermionBackend<W>
where
W: Write,
{
stdout: W,
writer: W,
}
impl<W> TermionBackend<W>
where
W: Write,
{
/// Creates a new Termion backend with the given output.
pub fn new(stdout: W) -> TermionBackend<W> {
TermionBackend { stdout }
/// Creates a new Termion backend with the given writer.
///
/// # Example
///
/// ```rust,no_run
/// # use std::io::stdout;
/// # use ratatui::prelude::*;
/// let backend = TermionBackend::new(stdout());
/// ```
pub fn new(writer: W) -> TermionBackend<W> {
TermionBackend { writer }
}
}
@@ -54,11 +92,11 @@ where
W: Write,
{
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.stdout.write(buf)
self.writer.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.stdout.flush()
self.writer.flush()
}
}
@@ -72,39 +110,39 @@ where
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)?,
ClearType::All => write!(self.writer, "{}", termion::clear::All)?,
ClearType::AfterCursor => write!(self.writer, "{}", termion::clear::AfterCursor)?,
ClearType::BeforeCursor => write!(self.writer, "{}", termion::clear::BeforeCursor)?,
ClearType::CurrentLine => write!(self.writer, "{}", termion::clear::CurrentLine)?,
ClearType::UntilNewLine => write!(self.writer, "{}", termion::clear::UntilNewline)?,
};
self.stdout.flush()
self.writer.flush()
}
fn append_lines(&mut self, n: u16) -> io::Result<()> {
for _ in 0..n {
writeln!(self.stdout)?;
writeln!(self.writer)?;
}
self.stdout.flush()
self.writer.flush()
}
fn hide_cursor(&mut self) -> io::Result<()> {
write!(self.stdout, "{}", termion::cursor::Hide)?;
self.stdout.flush()
write!(self.writer, "{}", termion::cursor::Hide)?;
self.writer.flush()
}
fn show_cursor(&mut self) -> io::Result<()> {
write!(self.stdout, "{}", termion::cursor::Show)?;
self.stdout.flush()
write!(self.writer, "{}", termion::cursor::Show)?;
self.writer.flush()
}
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
termion::cursor::DetectCursorPos::cursor_pos(&mut self.stdout).map(|(x, y)| (x - 1, y - 1))
termion::cursor::DetectCursorPos::cursor_pos(&mut self.writer).map(|(x, y)| (x - 1, y - 1))
}
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
write!(self.stdout, "{}", termion::cursor::Goto(x + 1, y + 1))?;
self.stdout.flush()
write!(self.writer, "{}", termion::cursor::Goto(x + 1, y + 1))?;
self.writer.flush()
}
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
@@ -144,10 +182,10 @@ where
write!(string, "{}", Bg(cell.bg)).unwrap();
bg = cell.bg;
}
string.push_str(&cell.symbol);
string.push_str(cell.symbol());
}
write!(
self.stdout,
self.writer,
"{string}{}{}{}",
Fg(Color::Reset),
Bg(Color::Reset),
@@ -160,8 +198,15 @@ where
Ok(Rect::new(0, 0, terminal.0, terminal.1))
}
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
Ok(WindowSize {
columns_rows: termion::terminal_size()?.into(),
pixels: termion::terminal_size_pixels()?.into(),
})
}
fn flush(&mut self) -> io::Result<()> {
self.stdout.flush()
self.writer.flush()
}
}
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
@@ -232,6 +277,82 @@ impl fmt::Display for Bg {
}
}
macro_rules! from_termion_for_color {
($termion_color:ident, $color: ident) => {
impl From<tcolor::$termion_color> for Color {
fn from(_: tcolor::$termion_color) -> Self {
Color::$color
}
}
impl From<tcolor::Bg<tcolor::$termion_color>> for Style {
fn from(_: tcolor::Bg<tcolor::$termion_color>) -> Self {
Style::default().bg(Color::$color)
}
}
impl From<tcolor::Fg<tcolor::$termion_color>> for Style {
fn from(_: tcolor::Fg<tcolor::$termion_color>) -> Self {
Style::default().fg(Color::$color)
}
}
};
}
from_termion_for_color!(Reset, Reset);
from_termion_for_color!(Black, Black);
from_termion_for_color!(Red, Red);
from_termion_for_color!(Green, Green);
from_termion_for_color!(Yellow, Yellow);
from_termion_for_color!(Blue, Blue);
from_termion_for_color!(Magenta, Magenta);
from_termion_for_color!(Cyan, Cyan);
from_termion_for_color!(White, Gray);
from_termion_for_color!(LightBlack, DarkGray);
from_termion_for_color!(LightRed, LightRed);
from_termion_for_color!(LightGreen, LightGreen);
from_termion_for_color!(LightBlue, LightBlue);
from_termion_for_color!(LightYellow, LightYellow);
from_termion_for_color!(LightMagenta, LightMagenta);
from_termion_for_color!(LightCyan, LightCyan);
from_termion_for_color!(LightWhite, White);
impl From<tcolor::AnsiValue> for Color {
fn from(value: tcolor::AnsiValue) -> Self {
Color::Indexed(value.0)
}
}
impl From<tcolor::Bg<tcolor::AnsiValue>> for Style {
fn from(value: tcolor::Bg<tcolor::AnsiValue>) -> Self {
Style::default().bg(Color::Indexed(value.0 .0))
}
}
impl From<tcolor::Fg<tcolor::AnsiValue>> for Style {
fn from(value: tcolor::Fg<tcolor::AnsiValue>) -> Self {
Style::default().fg(Color::Indexed(value.0 .0))
}
}
impl From<tcolor::Rgb> for Color {
fn from(value: tcolor::Rgb) -> Self {
Color::Rgb(value.0, value.1, value.2)
}
}
impl From<tcolor::Bg<tcolor::Rgb>> for Style {
fn from(value: tcolor::Bg<tcolor::Rgb>) -> Self {
Style::default().bg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
}
}
impl From<tcolor::Fg<tcolor::Rgb>> for Style {
fn from(value: tcolor::Fg<tcolor::Rgb>) -> Self {
Style::default().fg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
}
}
impl fmt::Display for ModifierDiff {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let remove = self.from - self.to;
@@ -296,3 +417,147 @@ impl fmt::Display for ModifierDiff {
Ok(())
}
}
macro_rules! from_termion_for_modifier {
($termion_modifier:ident, $modifier: ident) => {
impl From<tstyle::$termion_modifier> for Modifier {
fn from(_: tstyle::$termion_modifier) -> Self {
Modifier::$modifier
}
}
};
}
from_termion_for_modifier!(Invert, REVERSED);
from_termion_for_modifier!(Bold, BOLD);
from_termion_for_modifier!(Italic, ITALIC);
from_termion_for_modifier!(Underline, UNDERLINED);
from_termion_for_modifier!(Faint, DIM);
from_termion_for_modifier!(CrossedOut, CROSSED_OUT);
from_termion_for_modifier!(Blink, SLOW_BLINK);
impl From<termion::style::Reset> for Modifier {
fn from(_: termion::style::Reset) -> Self {
Modifier::empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Stylize;
#[test]
fn from_termion_color() {
assert_eq!(Color::from(tcolor::Reset), Color::Reset);
assert_eq!(Color::from(tcolor::Black), Color::Black);
assert_eq!(Color::from(tcolor::Red), Color::Red);
assert_eq!(Color::from(tcolor::Green), Color::Green);
assert_eq!(Color::from(tcolor::Yellow), Color::Yellow);
assert_eq!(Color::from(tcolor::Blue), Color::Blue);
assert_eq!(Color::from(tcolor::Magenta), Color::Magenta);
assert_eq!(Color::from(tcolor::Cyan), Color::Cyan);
assert_eq!(Color::from(tcolor::White), Color::Gray);
assert_eq!(Color::from(tcolor::LightBlack), Color::DarkGray);
assert_eq!(Color::from(tcolor::LightRed), Color::LightRed);
assert_eq!(Color::from(tcolor::LightGreen), Color::LightGreen);
assert_eq!(Color::from(tcolor::LightBlue), Color::LightBlue);
assert_eq!(Color::from(tcolor::LightYellow), Color::LightYellow);
assert_eq!(Color::from(tcolor::LightMagenta), Color::LightMagenta);
assert_eq!(Color::from(tcolor::LightCyan), Color::LightCyan);
assert_eq!(Color::from(tcolor::LightWhite), Color::White);
assert_eq!(Color::from(tcolor::AnsiValue(31)), Color::Indexed(31));
assert_eq!(Color::from(tcolor::Rgb(1, 2, 3)), Color::Rgb(1, 2, 3));
}
#[test]
fn from_termion_bg() {
use tc::Bg;
use tcolor as tc;
assert_eq!(Style::from(Bg(tc::Reset)), Style::new().bg(Color::Reset));
assert_eq!(Style::from(Bg(tc::Black)), Style::new().on_black());
assert_eq!(Style::from(Bg(tc::Red)), Style::new().on_red());
assert_eq!(Style::from(Bg(tc::Green)), Style::new().on_green());
assert_eq!(Style::from(Bg(tc::Yellow)), Style::new().on_yellow());
assert_eq!(Style::from(Bg(tc::Blue)), Style::new().on_blue());
assert_eq!(Style::from(Bg(tc::Magenta)), Style::new().on_magenta());
assert_eq!(Style::from(Bg(tc::Cyan)), Style::new().on_cyan());
assert_eq!(Style::from(Bg(tc::White)), Style::new().on_gray());
assert_eq!(Style::from(Bg(tc::LightBlack)), Style::new().on_dark_gray());
assert_eq!(Style::from(Bg(tc::LightRed)), Style::new().on_light_red());
assert_eq!(
Style::from(Bg(tc::LightGreen)),
Style::new().on_light_green()
);
assert_eq!(Style::from(Bg(tc::LightBlue)), Style::new().on_light_blue());
assert_eq!(
Style::from(Bg(tc::LightYellow)),
Style::new().on_light_yellow()
);
assert_eq!(
Style::from(Bg(tc::LightMagenta)),
Style::new().on_light_magenta()
);
assert_eq!(Style::from(Bg(tc::LightCyan)), Style::new().on_light_cyan());
assert_eq!(Style::from(Bg(tc::LightWhite)), Style::new().on_white());
assert_eq!(
Style::from(Bg(tc::AnsiValue(31))),
Style::new().bg(Color::Indexed(31))
);
assert_eq!(
Style::from(Bg(tc::Rgb(1, 2, 3))),
Style::new().bg(Color::Rgb(1, 2, 3))
);
}
#[test]
fn from_termion_fg() {
use tc::Fg;
use tcolor as tc;
assert_eq!(Style::from(Fg(tc::Reset)), Style::new().fg(Color::Reset));
assert_eq!(Style::from(Fg(tc::Black)), Style::new().black());
assert_eq!(Style::from(Fg(tc::Red)), Style::new().red());
assert_eq!(Style::from(Fg(tc::Green)), Style::new().green());
assert_eq!(Style::from(Fg(tc::Yellow)), Style::new().yellow());
assert_eq!(Style::from(Fg(tc::Blue)), Style::default().blue());
assert_eq!(Style::from(Fg(tc::Magenta)), Style::default().magenta());
assert_eq!(Style::from(Fg(tc::Cyan)), Style::default().cyan());
assert_eq!(Style::from(Fg(tc::White)), Style::default().gray());
assert_eq!(Style::from(Fg(tc::LightBlack)), Style::new().dark_gray());
assert_eq!(Style::from(Fg(tc::LightRed)), Style::new().light_red());
assert_eq!(Style::from(Fg(tc::LightGreen)), Style::new().light_green());
assert_eq!(Style::from(Fg(tc::LightBlue)), Style::new().light_blue());
assert_eq!(
Style::from(Fg(tc::LightYellow)),
Style::new().light_yellow()
);
assert_eq!(
Style::from(Fg(tc::LightMagenta)),
Style::new().light_magenta()
);
assert_eq!(Style::from(Fg(tc::LightCyan)), Style::new().light_cyan());
assert_eq!(Style::from(Fg(tc::LightWhite)), Style::new().white());
assert_eq!(
Style::from(Fg(tc::AnsiValue(31))),
Style::default().fg(Color::Indexed(31))
);
assert_eq!(
Style::from(Fg(tc::Rgb(1, 2, 3))),
Style::default().fg(Color::Rgb(1, 2, 3))
);
}
#[test]
fn from_termion_style() {
assert_eq!(Modifier::from(tstyle::Invert), Modifier::REVERSED);
assert_eq!(Modifier::from(tstyle::Bold), Modifier::BOLD);
assert_eq!(Modifier::from(tstyle::Italic), Modifier::ITALIC);
assert_eq!(Modifier::from(tstyle::Underline), Modifier::UNDERLINED);
assert_eq!(Modifier::from(tstyle::Faint), Modifier::DIM);
assert_eq!(Modifier::from(tstyle::CrossedOut), Modifier::CROSSED_OUT);
assert_eq!(Modifier::from(tstyle::Blink), Modifier::SLOW_BLINK);
assert_eq!(Modifier::from(tstyle::Reset), Modifier::empty());
}
}

View File

@@ -1,44 +1,89 @@
//! This module provides the `TermwizBackend` implementation for the [`Backend`] trait.
//! It uses the `termwiz` crate to interact with the terminal.
//! This module provides the `TermwizBackend` implementation for the [`Backend`] trait. It uses the
//! [Termwiz] crate to interact with the terminal.
//!
//! [`Backend`]: trait.Backend.html
//! [`TermwizBackend`]: crate::backend::TermionBackend
//! [Termwiz]: https://crates.io/crates/termwiz
use std::{error::Error, io};
use termwiz::{
caps::Capabilities,
cell::{AttributeChange, Blink, Intensity, Underline},
color::{AnsiColor, ColorAttribute, SrgbaTuple},
cell::{AttributeChange, Blink, CellAttributes, Intensity, Underline},
color::{AnsiColor, ColorAttribute, ColorSpec, LinearRgba, RgbColor, SrgbaTuple},
surface::{Change, CursorVisibility, Position},
terminal::{buffered::BufferedTerminal, SystemTerminal, Terminal},
terminal::{buffered::BufferedTerminal, ScreenSize, SystemTerminal, Terminal},
};
use crate::{
backend::Backend,
backend::{Backend, WindowSize},
buffer::Cell,
layout::Rect,
style::{Color, Modifier},
layout::Size,
prelude::Rect,
style::{Color, Modifier, Style},
};
/// Termwiz backend implementation for the [`Backend`] trait.
/// A [`Backend`] implementation that uses [Termwiz] to render to the terminal.
///
/// The `TermwizBackend` struct is a wrapper around a [`BufferedTerminal`], which is used to send
/// commands to the terminal. It provides methods for drawing content, manipulating the cursor, and
/// clearing the terminal screen.
///
/// Most applications should not call the methods on `TermwizBackend` directly, but will instead
/// use the [`Terminal`] struct, which provides a more ergonomic interface.
///
/// This backend automatically enables raw mode and switches to the alternate screen when it is
/// created using the [`TermwizBackend::new`] method (and disables raw mode and returns to the main
/// screen when dropped). Use the [`TermwizBackend::with_buffered_terminal`] to create a new
/// instance with a custom [`BufferedTerminal`] if this is not desired.
///
/// # Example
///
/// ```rust,no_run
/// use ratatui::backend::{Backend, TermwizBackend};
/// use ratatui::prelude::*;
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut backend = TermwizBackend::new()?;
/// backend.clear()?;
/// # Ok(())
/// # }
/// let backend = TermwizBackend::new()?;
/// let mut terminal = Terminal::new(backend)?;
///
/// terminal.clear()?;
/// terminal.draw(|frame| {
/// // -- snip --
/// })?;
/// # std::result::Result::Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// See the the [examples] directory for more examples. See the [`backend`] module documentation
/// for more details on raw mode and alternate screen.
///
/// [`backend`]: crate::backend
/// [`Terminal`]: crate::terminal::Terminal
/// [`BufferedTerminal`]: termwiz::terminal::buffered::BufferedTerminal
/// [Termwiz]: https://crates.io/crates/termwiz
/// [examples]: https://github.com/ratatui-org/ratatui/tree/main/examples#readme
pub struct TermwizBackend {
buffered_terminal: BufferedTerminal<SystemTerminal>,
}
impl TermwizBackend {
/// Creates a new Termwiz backend instance.
///
/// The backend will automatically enable raw mode and enter the alternate screen.
///
/// # Errors
///
/// Returns an error if unable to do any of the following:
/// - query the terminal capabilities.
/// - enter raw mode.
/// - enter the alternate screen.
/// - create the system or buffered terminal.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::prelude::*;
/// let backend = TermwizBackend::new()?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
pub fn new() -> Result<TermwizBackend, Box<dyn Error>> {
let mut buffered_terminal =
BufferedTerminal::new(SystemTerminal::new(Capabilities::new_from_env()?)?)?;
@@ -131,7 +176,7 @@ impl Backend for TermwizBackend {
},
)));
self.buffered_terminal.add_change(&cell.symbol);
self.buffered_terminal.add_change(cell.symbol());
}
Ok(())
}
@@ -169,22 +214,31 @@ impl Backend for TermwizBackend {
}
fn size(&self) -> Result<Rect, io::Error> {
let (term_width, term_height) = self.buffered_terminal.dimensions();
let max = u16::max_value();
Ok(Rect::new(
0,
0,
if term_width > usize::from(max) {
max
} else {
term_width as u16
let (cols, rows) = self.buffered_terminal.dimensions();
Ok(Rect::new(0, 0, u16_max(cols), u16_max(rows)))
}
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
let ScreenSize {
cols,
rows,
xpixel,
ypixel,
} = self
.buffered_terminal
.terminal()
.get_screen_size()
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
Ok(WindowSize {
columns_rows: Size {
width: u16_max(cols),
height: u16_max(rows),
},
if term_height > usize::from(max) {
max
} else {
term_height as u16
pixels: Size {
width: u16_max(xpixel),
height: u16_max(ypixel),
},
))
})
}
fn flush(&mut self) -> Result<(), io::Error> {
@@ -195,12 +249,73 @@ impl Backend for TermwizBackend {
}
}
impl From<CellAttributes> for Style {
fn from(value: CellAttributes) -> Self {
let mut style = Style::new()
.add_modifier(value.intensity().into())
.add_modifier(value.underline().into())
.add_modifier(value.blink().into());
if value.italic() {
style.add_modifier |= Modifier::ITALIC;
}
if value.reverse() {
style.add_modifier |= Modifier::REVERSED;
}
if value.strikethrough() {
style.add_modifier |= Modifier::CROSSED_OUT;
}
if value.invisible() {
style.add_modifier |= Modifier::HIDDEN;
}
style.fg = Some(value.foreground().into());
style.bg = Some(value.background().into());
#[cfg(feature = "underline_color")]
{
style.underline_color = Some(value.underline_color().into());
}
style
}
}
impl From<Intensity> for Modifier {
fn from(value: Intensity) -> Self {
match value {
Intensity::Normal => Modifier::empty(),
Intensity::Bold => Modifier::BOLD,
Intensity::Half => Modifier::DIM,
}
}
}
impl From<Underline> for Modifier {
fn from(value: Underline) -> Self {
match value {
Underline::None => Modifier::empty(),
_ => Modifier::UNDERLINED,
}
}
}
impl From<Blink> for Modifier {
fn from(value: Blink) -> Self {
match value {
Blink::None => Modifier::empty(),
Blink::Slow => Modifier::SLOW_BLINK,
Blink::Rapid => Modifier::RAPID_BLINK,
}
}
}
impl From<Color> for ColorAttribute {
fn from(color: Color) -> ColorAttribute {
match color {
Color::Reset => ColorAttribute::Default,
Color::Black => AnsiColor::Black.into(),
Color::Gray | Color::DarkGray => AnsiColor::Grey.into(),
Color::DarkGray => AnsiColor::Grey.into(),
Color::Gray => AnsiColor::Silver.into(),
Color::Red => AnsiColor::Maroon.into(),
Color::LightRed => AnsiColor::Red.into(),
Color::Green => AnsiColor::Green.into(),
@@ -221,3 +336,327 @@ impl From<Color> for ColorAttribute {
}
}
}
impl From<AnsiColor> for Color {
fn from(value: AnsiColor) -> Self {
match value {
AnsiColor::Black => Color::Black,
AnsiColor::Grey => Color::DarkGray,
AnsiColor::Silver => Color::Gray,
AnsiColor::Maroon => Color::Red,
AnsiColor::Red => Color::LightRed,
AnsiColor::Green => Color::Green,
AnsiColor::Lime => Color::LightGreen,
AnsiColor::Olive => Color::Yellow,
AnsiColor::Yellow => Color::LightYellow,
AnsiColor::Purple => Color::Magenta,
AnsiColor::Fuchsia => Color::LightMagenta,
AnsiColor::Teal => Color::Cyan,
AnsiColor::Aqua => Color::LightCyan,
AnsiColor::White => Color::White,
AnsiColor::Navy => Color::Blue,
AnsiColor::Blue => Color::LightBlue,
}
}
}
impl From<ColorAttribute> for Color {
fn from(value: ColorAttribute) -> Self {
match value {
ColorAttribute::TrueColorWithDefaultFallback(srgba)
| ColorAttribute::TrueColorWithPaletteFallback(srgba, _) => srgba.into(),
ColorAttribute::PaletteIndex(i) => Color::Indexed(i),
ColorAttribute::Default => Color::Reset,
}
}
}
impl From<ColorSpec> for Color {
fn from(value: ColorSpec) -> Self {
match value {
ColorSpec::Default => Color::Reset,
ColorSpec::PaletteIndex(i) => Color::Indexed(i),
ColorSpec::TrueColor(srgba) => srgba.into(),
}
}
}
impl From<SrgbaTuple> for Color {
fn from(value: SrgbaTuple) -> Self {
let (r, g, b, _) = value.to_srgb_u8();
Color::Rgb(r, g, b)
}
}
impl From<RgbColor> for Color {
fn from(value: RgbColor) -> Self {
let (r, g, b) = value.to_tuple_rgb8();
Color::Rgb(r, g, b)
}
}
impl From<LinearRgba> for Color {
fn from(value: LinearRgba) -> Self {
value.to_srgb().into()
}
}
#[inline]
fn u16_max(i: usize) -> u16 {
u16::try_from(i).unwrap_or(u16::MAX)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Stylize;
mod into_color {
use Color as C;
use super::*;
#[test]
fn from_linear_rgba() {
// full black + opaque
assert_eq!(C::from(LinearRgba(0., 0., 0., 1.)), Color::Rgb(0, 0, 0));
// full black + transparent
assert_eq!(C::from(LinearRgba(0., 0., 0., 0.)), Color::Rgb(0, 0, 0));
// full white + opaque
assert_eq!(C::from(LinearRgba(1., 1., 1., 1.)), C::Rgb(254, 254, 254));
// full white + transparent
assert_eq!(C::from(LinearRgba(1., 1., 1., 0.)), C::Rgb(254, 254, 254));
// full red
assert_eq!(C::from(LinearRgba(1., 0., 0., 1.)), C::Rgb(254, 0, 0));
// full green
assert_eq!(C::from(LinearRgba(0., 1., 0., 1.)), C::Rgb(0, 254, 0));
// full blue
assert_eq!(C::from(LinearRgba(0., 0., 1., 1.)), C::Rgb(0, 0, 254));
// See https://stackoverflow.com/questions/12524623/what-are-the-practical-differences-when-working-with-colors-in-a-linear-vs-a-no
// for an explanation
// half red
assert_eq!(C::from(LinearRgba(0.214, 0., 0., 1.)), C::Rgb(127, 0, 0));
// half green
assert_eq!(C::from(LinearRgba(0., 0.214, 0., 1.)), C::Rgb(0, 127, 0));
// half blue
assert_eq!(C::from(LinearRgba(0., 0., 0.214, 1.)), C::Rgb(0, 0, 127));
}
#[test]
fn from_srgba() {
// full black + opaque
assert_eq!(C::from(SrgbaTuple(0., 0., 0., 1.)), Color::Rgb(0, 0, 0));
// full black + transparent
assert_eq!(C::from(SrgbaTuple(0., 0., 0., 0.)), Color::Rgb(0, 0, 0));
// full white + opaque
assert_eq!(C::from(SrgbaTuple(1., 1., 1., 1.)), C::Rgb(255, 255, 255));
// full white + transparent
assert_eq!(C::from(SrgbaTuple(1., 1., 1., 0.)), C::Rgb(255, 255, 255));
// full red
assert_eq!(C::from(SrgbaTuple(1., 0., 0., 1.)), C::Rgb(255, 0, 0));
// full green
assert_eq!(C::from(SrgbaTuple(0., 1., 0., 1.)), C::Rgb(0, 255, 0));
// full blue
assert_eq!(C::from(SrgbaTuple(0., 0., 1., 1.)), C::Rgb(0, 0, 255));
// half red
assert_eq!(C::from(SrgbaTuple(0.5, 0., 0., 1.)), C::Rgb(127, 0, 0));
// half green
assert_eq!(C::from(SrgbaTuple(0., 0.5, 0., 1.)), C::Rgb(0, 127, 0));
// half blue
assert_eq!(C::from(SrgbaTuple(0., 0., 0.5, 1.)), C::Rgb(0, 0, 127));
}
#[test]
fn from_rgbcolor() {
// full black
assert_eq!(C::from(RgbColor::new_8bpc(0, 0, 0)), Color::Rgb(0, 0, 0));
// full white
assert_eq!(
C::from(RgbColor::new_8bpc(255, 255, 255)),
C::Rgb(255, 255, 255)
);
// full red
assert_eq!(C::from(RgbColor::new_8bpc(255, 0, 0)), C::Rgb(255, 0, 0));
// full green
assert_eq!(C::from(RgbColor::new_8bpc(0, 255, 0)), C::Rgb(0, 255, 0));
// full blue
assert_eq!(C::from(RgbColor::new_8bpc(0, 0, 255)), C::Rgb(0, 0, 255));
// half red
assert_eq!(C::from(RgbColor::new_8bpc(127, 0, 0)), C::Rgb(127, 0, 0));
// half green
assert_eq!(C::from(RgbColor::new_8bpc(0, 127, 0)), C::Rgb(0, 127, 0));
// half blue
assert_eq!(C::from(RgbColor::new_8bpc(0, 0, 127)), C::Rgb(0, 0, 127));
}
#[test]
fn from_colorspec() {
assert_eq!(C::from(ColorSpec::Default), C::Reset);
assert_eq!(C::from(ColorSpec::PaletteIndex(33)), C::Indexed(33));
assert_eq!(
C::from(ColorSpec::TrueColor(SrgbaTuple(0., 0., 0., 1.))),
C::Rgb(0, 0, 0)
);
}
#[test]
fn from_colorattribute() {
assert_eq!(C::from(ColorAttribute::Default), C::Reset);
assert_eq!(C::from(ColorAttribute::PaletteIndex(32)), C::Indexed(32));
assert_eq!(
C::from(ColorAttribute::TrueColorWithDefaultFallback(SrgbaTuple(
0., 0., 0., 1.
))),
C::Rgb(0, 0, 0)
);
assert_eq!(
C::from(ColorAttribute::TrueColorWithPaletteFallback(
SrgbaTuple(0., 0., 0., 1.),
31
)),
C::Rgb(0, 0, 0)
);
}
#[test]
fn from_ansicolor() {
assert_eq!(C::from(AnsiColor::Black), Color::Black);
assert_eq!(C::from(AnsiColor::Grey), Color::DarkGray);
assert_eq!(C::from(AnsiColor::Silver), Color::Gray);
assert_eq!(C::from(AnsiColor::Maroon), Color::Red);
assert_eq!(C::from(AnsiColor::Red), Color::LightRed);
assert_eq!(C::from(AnsiColor::Green), Color::Green);
assert_eq!(C::from(AnsiColor::Lime), Color::LightGreen);
assert_eq!(C::from(AnsiColor::Olive), Color::Yellow);
assert_eq!(C::from(AnsiColor::Yellow), Color::LightYellow);
assert_eq!(C::from(AnsiColor::Purple), Color::Magenta);
assert_eq!(C::from(AnsiColor::Fuchsia), Color::LightMagenta);
assert_eq!(C::from(AnsiColor::Teal), Color::Cyan);
assert_eq!(C::from(AnsiColor::Aqua), Color::LightCyan);
assert_eq!(C::from(AnsiColor::White), Color::White);
assert_eq!(C::from(AnsiColor::Navy), Color::Blue);
assert_eq!(C::from(AnsiColor::Blue), Color::LightBlue);
}
}
mod into_modifier {
use super::*;
#[test]
fn from_intensity() {
assert_eq!(Modifier::from(Intensity::Normal), Modifier::empty());
assert_eq!(Modifier::from(Intensity::Bold), Modifier::BOLD);
assert_eq!(Modifier::from(Intensity::Half), Modifier::DIM);
}
#[test]
fn from_underline() {
assert_eq!(Modifier::from(Underline::None), Modifier::empty());
assert_eq!(Modifier::from(Underline::Single), Modifier::UNDERLINED);
assert_eq!(Modifier::from(Underline::Double), Modifier::UNDERLINED);
assert_eq!(Modifier::from(Underline::Curly), Modifier::UNDERLINED);
assert_eq!(Modifier::from(Underline::Dashed), Modifier::UNDERLINED);
assert_eq!(Modifier::from(Underline::Dotted), Modifier::UNDERLINED);
}
#[test]
fn from_blink() {
assert_eq!(Modifier::from(Blink::None), Modifier::empty());
assert_eq!(Modifier::from(Blink::Slow), Modifier::SLOW_BLINK);
assert_eq!(Modifier::from(Blink::Rapid), Modifier::RAPID_BLINK);
}
}
#[test]
fn from_cell_attribute_for_style() {
// default
assert_eq!(
Style::from(CellAttributes::default()),
Style::new().fg(Color::Reset).bg(Color::Reset)
);
// foreground color
assert_eq!(
Style::from(
CellAttributes::default()
.set_foreground(ColorAttribute::PaletteIndex(31))
.to_owned()
),
Style::new().fg(Color::Indexed(31)).bg(Color::Reset)
);
// background color
assert_eq!(
Style::from(
CellAttributes::default()
.set_background(ColorAttribute::PaletteIndex(31))
.to_owned()
),
Style::new().fg(Color::Reset).bg(Color::Indexed(31))
);
// underline color
#[cfg(feature = "underline_color")]
assert_eq!(
Style::from(
CellAttributes::default()
.set_underline_color(AnsiColor::Red)
.set
.to_owned()
),
Style::new()
.fg(Color::Reset)
.bg(Color::Reset)
.underline_color(Color::Red)
);
// underlined
assert_eq!(
Style::from(
CellAttributes::default()
.set_underline(Underline::Single)
.to_owned()
),
Style::new().fg(Color::Reset).bg(Color::Reset).underlined()
);
// blink
assert_eq!(
Style::from(CellAttributes::default().set_blink(Blink::Slow).to_owned()),
Style::new().fg(Color::Reset).bg(Color::Reset).slow_blink()
);
// intensity
assert_eq!(
Style::from(
CellAttributes::default()
.set_intensity(Intensity::Bold)
.to_owned()
),
Style::new().fg(Color::Reset).bg(Color::Reset).bold()
);
// italic
assert_eq!(
Style::from(CellAttributes::default().set_italic(true).to_owned()),
Style::new().fg(Color::Reset).bg(Color::Reset).italic()
);
// reversed
assert_eq!(
Style::from(CellAttributes::default().set_reverse(true).to_owned()),
Style::new().fg(Color::Reset).bg(Color::Reset).reversed()
);
// strikethrough
assert_eq!(
Style::from(CellAttributes::default().set_strikethrough(true).to_owned()),
Style::new().fg(Color::Reset).bg(Color::Reset).crossed_out()
);
// hidden
assert_eq!(
Style::from(CellAttributes::default().set_invisible(true).to_owned()),
Style::new().fg(Color::Reset).bg(Color::Reset).hidden()
);
}
}

View File

@@ -9,24 +9,28 @@ use std::{
use unicode_width::UnicodeWidthStr;
use crate::{
backend::Backend,
backend::{Backend, ClearType, WindowSize},
buffer::{Buffer, Cell},
layout::Rect,
layout::{Rect, Size},
};
/// A backend used for the integration tests.
/// A [`Backend`] implementation used for integration testing that that renders to an in memory
/// buffer.
///
/// Note: that although many of the integration and unit tests in ratatui are written using this
/// backend, it is preferable to write unit tests for widgets directly against the buffer rather
/// than using this backend. This backend is intended for integration tests that test the entire
/// terminal UI.
///
/// # Example
///
/// ```rust
/// use ratatui::{backend::{Backend, TestBackend}, buffer::Buffer};
/// use ratatui::{backend::TestBackend, prelude::*};
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut backend = TestBackend::new(10, 2);
/// backend.clear()?;
/// backend.assert_buffer(&Buffer::with_lines(vec![" "; 2]));
/// # Ok(())
/// # }
/// # std::io::Result::Ok(())
/// ```
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@@ -52,11 +56,11 @@ fn buffer_view(buffer: &Buffer) -> String {
view.push('"');
for (x, c) in cells.iter().enumerate() {
if skip == 0 {
view.push_str(&c.symbol);
view.push_str(c.symbol());
} else {
overwritten.push((x, &c.symbol));
overwritten.push((x, c.symbol()));
}
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
skip = std::cmp::max(skip, c.symbol().width()).saturating_sub(1);
}
view.push('"');
if !overwritten.is_empty() {
@@ -175,10 +179,87 @@ impl Backend for TestBackend {
Ok(())
}
fn clear_region(&mut self, clear_type: super::ClearType) -> io::Result<()> {
match clear_type {
ClearType::All => self.clear()?,
ClearType::AfterCursor => {
let index = self.buffer.index_of(self.pos.0, self.pos.1) + 1;
self.buffer.content[index..].fill(Cell::default());
}
ClearType::BeforeCursor => {
let index = self.buffer.index_of(self.pos.0, self.pos.1);
self.buffer.content[..index].fill(Cell::default());
}
ClearType::CurrentLine => {
let line_start_index = self.buffer.index_of(0, self.pos.1);
let line_end_index = self.buffer.index_of(self.width - 1, self.pos.1);
self.buffer.content[line_start_index..=line_end_index].fill(Cell::default());
}
ClearType::UntilNewLine => {
let index = self.buffer.index_of(self.pos.0, self.pos.1);
let line_end_index = self.buffer.index_of(self.width - 1, self.pos.1);
self.buffer.content[index..=line_end_index].fill(Cell::default());
}
}
Ok(())
}
/// Inserts n line breaks at the current cursor position.
///
/// After the insertion, the cursor x position will be incremented by 1 (unless it's already
/// at the end of line). This is a common behaviour of terminals in raw mode.
///
/// If the number of lines to append is fewer than the number of lines in the buffer after the
/// cursor y position then the cursor is moved down by n rows.
///
/// If the number of lines to append is greater than the number of lines in the buffer after
/// the cursor y position then that number of empty lines (at most the buffer's height in this
/// case but this limit is instead replaced with scrolling in most backend implementations) will
/// be added after the current position and the cursor will be moved to the last row.
fn append_lines(&mut self, n: u16) -> io::Result<()> {
let (cur_x, cur_y) = self.get_cursor()?;
// the next column ensuring that we don't go past the last column
let new_cursor_x = cur_x.saturating_add(1).min(self.width.saturating_sub(1));
let max_y = self.height.saturating_sub(1);
let lines_after_cursor = max_y.saturating_sub(cur_y);
if n > lines_after_cursor {
let rotate_by = n.saturating_sub(lines_after_cursor).min(max_y);
if rotate_by == self.height - 1 {
self.clear()?;
}
self.set_cursor(0, rotate_by)?;
self.clear_region(ClearType::BeforeCursor)?;
self.buffer
.content
.rotate_left((self.width * rotate_by).into());
}
let new_cursor_y = cur_y.saturating_add(n).min(max_y);
self.set_cursor(new_cursor_x, new_cursor_y)?;
Ok(())
}
fn size(&self) -> Result<Rect, io::Error> {
Ok(Rect::new(0, 0, self.width, self.height))
}
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
// Some arbitrary window pixel size, probably doesn't need much testing.
static WINDOW_PIXEL_SIZE: Size = Size {
width: 640,
height: 480,
};
Ok(WindowSize {
columns_rows: (self.width, self.height).into(),
pixels: WINDOW_PIXEL_SIZE,
})
}
fn flush(&mut self) -> Result<(), io::Error> {
Ok(())
}
@@ -294,13 +375,299 @@ mod tests {
#[test]
fn clear() {
let mut backend = TestBackend::new(10, 2);
let mut backend = TestBackend::new(10, 4);
let mut cell = Cell::default();
cell.set_symbol("a");
backend.draw([(0, 0, &cell)].into_iter()).unwrap();
backend.draw([(0, 1, &cell)].into_iter()).unwrap();
backend.clear().unwrap();
backend.assert_buffer(&Buffer::with_lines(vec![" "; 2]));
backend.assert_buffer(&Buffer::with_lines(vec![
" ",
" ",
" ",
" ",
]));
}
#[test]
fn clear_region_all() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]);
backend.clear_region(ClearType::All).unwrap();
backend.assert_buffer(&Buffer::with_lines(vec![
" ",
" ",
" ",
" ",
" ",
]));
}
#[test]
fn clear_region_after_cursor() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]);
backend.set_cursor(3, 2).unwrap();
backend.clear_region(ClearType::AfterCursor).unwrap();
backend.assert_buffer(&Buffer::with_lines(vec![
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaa ",
" ",
" ",
]));
}
#[test]
fn clear_region_before_cursor() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]);
backend.set_cursor(5, 3).unwrap();
backend.clear_region(ClearType::BeforeCursor).unwrap();
backend.assert_buffer(&Buffer::with_lines(vec![
" ",
" ",
" ",
" aaaaa",
"aaaaaaaaaa",
]));
}
#[test]
fn clear_region_current_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]);
backend.set_cursor(3, 1).unwrap();
backend.clear_region(ClearType::CurrentLine).unwrap();
backend.assert_buffer(&Buffer::with_lines(vec![
"aaaaaaaaaa",
" ",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]));
}
#[test]
fn clear_region_until_new_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]);
backend.set_cursor(3, 0).unwrap();
backend.clear_region(ClearType::UntilNewLine).unwrap();
backend.assert_buffer(&Buffer::with_lines(vec![
"aaa ",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
]));
}
#[test]
fn append_lines_not_at_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
backend.set_cursor(0, 0).unwrap();
// If the cursor is not at the last line in the terminal the addition of a
// newline simply moves the cursor down and to the right
backend.append_lines(1).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 1));
backend.append_lines(1).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (2, 2));
backend.append_lines(1).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (3, 3));
backend.append_lines(1).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (4, 4));
// As such the buffer should remain unchanged
backend.assert_buffer(&Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]));
}
#[test]
fn append_lines_at_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
// If the cursor is at the last line in the terminal the addition of a
// newline will scroll the contents of the buffer
backend.set_cursor(0, 4).unwrap();
backend.append_lines(1).unwrap();
backend.buffer = Buffer::with_lines(vec![
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
" ",
]);
// It also moves the cursor to the right, as is common of the behaviour of
// terminals in raw-mode
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
}
#[test]
fn append_multiple_lines_not_at_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
backend.set_cursor(0, 0).unwrap();
// If the cursor is not at the last line in the terminal the addition of multiple
// newlines simply moves the cursor n lines down and to the right by 1
backend.append_lines(4).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
// As such the buffer should remain unchanged
backend.assert_buffer(&Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]));
}
#[test]
fn append_multiple_lines_past_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
backend.set_cursor(0, 3).unwrap();
backend.append_lines(3).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
backend.assert_buffer(&Buffer::with_lines(vec![
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
" ",
" ",
]));
}
#[test]
fn append_multiple_lines_where_cursor_at_end_appends_height_lines() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
backend.set_cursor(0, 4).unwrap();
backend.append_lines(5).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
backend.assert_buffer(&Buffer::with_lines(vec![
" ",
" ",
" ",
" ",
" ",
]));
}
#[test]
fn append_multiple_lines_where_cursor_appends_height_lines() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines(vec![
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
backend.set_cursor(0, 0).unwrap();
backend.append_lines(5).unwrap();
assert_eq!(backend.get_cursor().unwrap(), (1, 4));
backend.assert_buffer(&Buffer::with_lines(vec![
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
" ",
]));
}
#[test]

File diff suppressed because it is too large Load Diff

82
src/buffer/assert.rs Normal file
View File

@@ -0,0 +1,82 @@
/// 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);
indoc::formatdoc! {"
{i}: at ({x}, {y})
expected: {expected_cell:?}
actual: {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");
}
}
};
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
#[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);
}
}

891
src/buffer/buffer.rs Normal file
View File

@@ -0,0 +1,891 @@
use std::{
cmp::min,
fmt::{Debug, Formatter, Result},
};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::{buffer::Cell, prelude::*};
/// A buffer that maps to the desired content of the terminal after the draw call
///
/// No widget in the library interacts directly with the terminal. Instead each of them is required
/// to draw their state to an intermediate buffer. It is basically a grid where each cell contains
/// a grapheme, a foreground color and a background color. This grid will then be used to output
/// the appropriate escape sequences and characters to draw the UI as the user has defined it.
///
/// # Examples:
///
/// ```
/// use ratatui::{buffer::Cell, prelude::*};
///
/// let mut buf = Buffer::empty(Rect {
/// x: 0,
/// y: 0,
/// width: 10,
/// height: 5,
/// });
/// buf.get_mut(0, 2).set_symbol("x");
/// assert_eq!(buf.get(0, 2).symbol(), "x");
///
/// buf.set_string(
/// 3,
/// 0,
/// "string",
/// Style::default().fg(Color::Red).bg(Color::White),
/// );
/// let cell = buf.get_mut(5, 0);
/// assert_eq!(cell.symbol(), "r");
/// assert_eq!(cell.fg, Color::Red);
/// assert_eq!(cell.bg, Color::White);
///
/// buf.get_mut(5, 0).set_char('x');
/// assert_eq!(buf.get(5, 0).symbol(), "x");
/// ```
#[derive(Default, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Buffer {
/// The area represented by this buffer
pub area: Rect,
/// The content of the buffer. The length of this Vec should always be equal to area.width *
/// area.height
pub content: Vec<Cell>,
}
impl Buffer {
/// Returns a Buffer with all cells set to the default one
pub fn empty(area: Rect) -> Buffer {
let cell = Cell::default();
Buffer::filled(area, &cell)
}
/// Returns a Buffer with all cells initialized with the attributes of the given Cell
pub fn filled(area: Rect, cell: &Cell) -> Buffer {
let size = area.area() as usize;
let mut content = Vec::with_capacity(size);
for _ in 0..size {
content.push(cell.clone());
}
Buffer { area, content }
}
/// Returns a Buffer containing the given lines
pub fn with_lines<'a, S>(lines: Vec<S>) -> Buffer
where
S: Into<Line<'a>>,
{
let lines = lines.into_iter().map(Into::into).collect::<Vec<_>>();
let height = lines.len() as u16;
let width = lines.iter().map(Line::width).max().unwrap_or_default() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
for (y, line) in lines.iter().enumerate() {
buffer.set_line(0, y as u16, line, width);
}
buffer
}
/// Returns the content of the buffer as a slice
pub fn content(&self) -> &[Cell] {
&self.content
}
/// Returns the area covered by this buffer
pub fn area(&self) -> &Rect {
&self.area
}
/// Returns a reference to Cell at the given coordinates
pub fn get(&self, x: u16, y: u16) -> &Cell {
let i = self.index_of(x, y);
&self.content[i]
}
/// Returns a mutable reference to Cell at the given coordinates
pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell {
let i = self.index_of(x, y);
&mut self.content[i]
}
/// 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 ratatui::prelude::*;
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
/// // Global coordinates to the top corner of this buffer's area
/// assert_eq!(buffer.index_of(200, 100), 0);
/// ```
///
/// # Panics
///
/// Panics when given an coordinate that is outside of this Buffer's area.
///
/// ```should_panic
/// # use ratatui::prelude::*;
/// 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
/// // starts at (200, 100).
/// buffer.index_of(0, 0); // Panics
/// ```
pub fn index_of(&self, x: u16, y: u16) -> usize {
debug_assert!(
x >= self.area.left()
&& x < self.area.right()
&& y >= self.area.top()
&& y < self.area.bottom(),
"Trying to access position outside the buffer: x={x}, y={y}, area={:?}",
self.area
);
((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
}
/// Returns the (global) coordinates of a cell given its index
///
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
///
/// # Examples
///
/// ```
/// # use ratatui::prelude::*;
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
/// assert_eq!(buffer.pos_of(0), (200, 100));
/// assert_eq!(buffer.pos_of(14), (204, 101));
/// ```
///
/// # Panics
///
/// Panics when given an index that is outside the Buffer's content.
///
/// ```should_panic
/// # use ratatui::prelude::*;
/// 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.
/// buffer.pos_of(100); // Panics
/// ```
pub fn pos_of(&self, i: usize) -> (u16, u16) {
debug_assert!(
i < self.content.len(),
"Trying to get the coords of a cell outside the buffer: i={i} len={}",
self.content.len()
);
(
self.area.x + (i as u16) % self.area.width,
self.area.y + (i as u16) / self.area.width,
)
}
/// Print a string, starting at the position (x, y)
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
pub fn set_string<T, S>(&mut self, x: u16, y: u16, string: T, style: S)
where
T: AsRef<str>,
S: Into<Style>,
{
self.set_stringn(x, y, string, usize::MAX, style.into());
}
/// Print at most the first n characters of a string if enough space is available
/// until the end of the line
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
pub fn set_stringn<T, S>(
&mut self,
x: u16,
y: u16,
string: T,
width: usize,
style: S,
) -> (u16, u16)
where
T: AsRef<str>,
S: Into<Style>,
{
let style = style.into();
let mut index = self.index_of(x, y);
let mut x_offset = x as usize;
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
for s in graphemes {
let width = s.width();
if width == 0 {
continue;
}
// `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
// change dimensions to usize or u32 and someone resizes the terminal to 1x2^32.
if width > max_offset.saturating_sub(x_offset) {
break;
}
self.content[index].set_symbol(s);
self.content[index].set_style(style);
// Reset following cells if multi-width (they would be hidden by the grapheme),
for i in index + 1..index + width {
self.content[i].reset();
}
index += width;
x_offset += width;
}
(x_offset as u16, y)
}
/// Print a line, starting at the position (x, y)
pub fn set_line(&mut self, x: u16, y: u16, line: &Line<'_>, width: u16) -> (u16, u16) {
let mut remaining_width = width;
let mut x = x;
for span in &line.spans {
if remaining_width == 0 {
break;
}
let pos = self.set_stringn(
x,
y,
span.content.as_ref(),
remaining_width as usize,
span.style,
);
let w = pos.0.saturating_sub(x);
x = pos.0;
remaining_width = remaining_width.saturating_sub(w);
}
(x, y)
}
/// Print a span, starting at the position (x, y)
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)
}
/// Set the style of all cells in the given area.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
pub fn set_style<S: Into<Style>>(&mut self, area: Rect, style: S) {
let style = style.into();
let area = self.area.intersection(area);
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
self.get_mut(x, y).set_style(style);
}
}
}
/// Resize the buffer so that the mapped area matches the given area and that the buffer
/// length is equal to area.width * area.height
pub fn resize(&mut self, area: Rect) {
let length = area.area() as usize;
if self.content.len() > length {
self.content.truncate(length);
} else {
self.content.resize(length, Cell::default());
}
self.area = area;
}
/// Reset all cells in the buffer
pub fn reset(&mut self) {
for c in &mut self.content {
c.reset();
}
}
/// Merge an other buffer into this one
pub fn merge(&mut self, other: &Buffer) {
let area = self.area.union(other.area);
let cell = Cell::default();
self.content.resize(area.area() as usize, cell.clone());
// Move original content to the appropriate space
let size = self.area.area() as usize;
for i in (0..size).rev() {
let (x, y) = self.pos_of(i);
// New index in content
let k = ((y - area.y) * area.width + x - area.x) as usize;
if i != k {
self.content[k] = self.content[i].clone();
self.content[i] = cell.clone();
}
}
// Push content of the other buffer into this one (may erase previous
// data)
let size = other.area.area() as usize;
for i in 0..size {
let (x, y) = other.pos_of(i);
// New index in content
let k = ((y - area.y) * area.width + x - area.x) as usize;
self.content[k] = other.content[i].clone();
}
self.area = area;
}
/// Builds a minimal sequence of coordinates and Cells necessary to update the UI from
/// self to other.
///
/// We're assuming that buffers are well-formed, that is no double-width cell is followed by
/// a non-blank cell.
///
/// # Multi-width characters handling:
///
/// ```text
/// (Index:) `01`
/// Prev: `コ`
/// Next: `aa`
/// Updates: `0: a, 1: a'
/// ```
///
/// ```text
/// (Index:) `01`
/// Prev: `a `
/// Next: `コ`
/// Updates: `0: コ` (double width symbol at index 0 - skip index 1)
/// ```
///
/// ```text
/// (Index:) `012`
/// Prev: `aaa`
/// Next: `aコ`
/// Updates: `0: a, 1: コ` (double width symbol at index 1 - skip index 2)
/// ```
pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> {
let previous_buffer = &self.content;
let next_buffer = &other.content;
let mut updates: Vec<(u16, u16, &Cell)> = vec![];
// Cells invalidated by drawing/replacing preceding multi-width characters:
let mut invalidated: usize = 0;
// Cells from the current buffer to skip due to preceding multi-width characters taking
// their place (the skipped cells should be blank anyway), or due to per-cell-skipping:
let mut to_skip: usize = 0;
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
if !current.skip && (current != previous || invalidated > 0) && to_skip == 0 {
let (x, y) = self.pos_of(i);
updates.push((x, y, &next_buffer[i]));
}
to_skip = current.symbol().width().saturating_sub(1);
let affected_width = std::cmp::max(current.symbol().width(), previous.symbol().width());
invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
}
updates
}
}
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);
#[cfg(feature = "underline-color")]
{
let style = (c.fg, c.bg, c.underline_color, c.modifier);
if last_style != Some(style) {
last_style = Some(style);
styles.push((x, y, c.fg, c.bg, c.underline_color, c.modifier));
}
}
#[cfg(not(feature = "underline-color"))]
{
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 {
#[cfg(feature = "underline-color")]
f.write_fmt(format_args!(
" x: {}, y: {}, fg: {:?}, bg: {:?}, underline: {:?}, modifier: {:?},\n",
s.0, s.1, s.2, s.3, s.4, s.5
))?;
#[cfg(not(feature = "underline-color"))]
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::*;
use crate::assert_buffer_eq;
fn cell(s: &str) -> Cell {
let mut cell = Cell::default();
cell.set_symbol(s);
cell
}
#[test]
fn 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),
);
#[cfg(feature = "underline-color")]
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, underline: Reset, modifier: NONE,
x: 0, y: 1, fg: Green, bg: Yellow, underline: Reset, modifier: BOLD,
]
}"
)
);
#[cfg(not(feature = "underline-color"))]
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: NONE,
x: 0, y: 1, fg: Green, bg: Yellow, modifier: BOLD,
]
}"
)
);
}
#[test]
fn it_translates_to_and_from_coordinates() {
let rect = Rect::new(200, 100, 50, 80);
let buf = Buffer::empty(rect);
// First cell is at the upper left corner.
assert_eq!(buf.pos_of(0), (200, 100));
assert_eq!(buf.index_of(200, 100), 0);
// Last cell is in the lower right.
assert_eq!(buf.pos_of(buf.content.len() - 1), (249, 179));
assert_eq!(buf.index_of(249, 179), buf.content.len() - 1);
}
#[test]
#[should_panic(expected = "outside the buffer")]
fn pos_of_panics_on_out_of_bounds() {
let rect = Rect::new(0, 0, 10, 10);
let buf = Buffer::empty(rect);
// There are a total of 100 cells; zero-indexed means that 100 would be the 101st cell.
buf.pos_of(100);
}
#[test]
#[should_panic(expected = "outside the buffer")]
fn index_of_panics_on_out_of_bounds() {
let rect = Rect::new(0, 0, 10, 10);
let buf = Buffer::empty(rect);
// width is 10; zero-indexed means that 10 would be the 11th cell.
buf.index_of(10, 0);
}
#[test]
fn set_string() {
let area = Rect::new(0, 0, 5, 1);
let mut buffer = Buffer::empty(area);
// Zero-width
buffer.set_stringn(0, 0, "aaa", 0, Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" "]));
buffer.set_string(0, 0, "aaa", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
// Width limit:
buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
buffer.set_string(0, 0, "12345", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
// Width truncation:
buffer.set_string(0, 0, "123456", Style::default());
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 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]
fn set_string_zero_width() {
let area = Rect::new(0, 0, 1, 1);
let mut buffer = Buffer::empty(area);
// Leading grapheme with zero width
let s = "\u{1}a";
buffer.set_stringn(0, 0, s, 1, Style::default());
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_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
}
#[test]
fn set_string_double_width() {
let area = Rect::new(0, 0, 5, 1);
let mut buffer = Buffer::empty(area);
buffer.set_string(0, 0, "コン", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
// Only 1 space left.
buffer.set_string(0, 0, "コンピ", Style::default());
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
}
#[test]
fn set_style() {
let mut buffer = Buffer::with_lines(vec!["aaaaa", "bbbbb", "ccccc"]);
buffer.set_style(Rect::new(0, 1, 5, 1), Style::new().red());
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec!["aaaaa".into(), "bbbbb".red(), "ccccc".into(),])
);
}
#[test]
fn set_style_does_not_panic_when_out_of_area() {
let mut buffer = Buffer::with_lines(vec!["aaaaa", "bbbbb", "ccccc"]);
buffer.set_style(Rect::new(0, 1, 10, 3), Style::new().red());
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec!["aaaaa".into(), "bbbbb".red(), "ccccc".red(),])
);
}
#[test]
fn with_lines() {
let buffer =
Buffer::with_lines(vec!["┌────────┐", "│コンピュ│", "│ーa 上で│", "└────────┘"]);
assert_eq!(buffer.area.x, 0);
assert_eq!(buffer.area.y, 0);
assert_eq!(buffer.area.width, 10);
assert_eq!(buffer.area.height, 4);
}
#[test]
fn diff_empty_empty() {
let area = Rect::new(0, 0, 40, 40);
let prev = Buffer::empty(area);
let next = Buffer::empty(area);
let diff = prev.diff(&next);
assert_eq!(diff, vec![]);
}
#[test]
fn diff_empty_filled() {
let area = Rect::new(0, 0, 40, 40);
let prev = Buffer::empty(area);
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
let diff = prev.diff(&next);
assert_eq!(diff.len(), 40 * 40);
}
#[test]
fn diff_filled_filled() {
let area = Rect::new(0, 0, 40, 40);
let prev = Buffer::filled(area, Cell::default().set_symbol("a"));
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
let diff = prev.diff(&next);
assert_eq!(diff, vec![]);
}
#[test]
fn diff_single_width() {
let prev = Buffer::with_lines(vec![
" ",
"┌Title─┐ ",
"│ │ ",
"│ │ ",
"└──────┘ ",
]);
let next = Buffer::with_lines(vec![
" ",
"┌TITLE─┐ ",
"│ │ ",
"│ │ ",
"└──────┘ ",
]);
let diff = prev.diff(&next);
assert_eq!(
diff,
vec![
(2, 1, &cell("I")),
(3, 1, &cell("T")),
(4, 1, &cell("L")),
(5, 1, &cell("E")),
]
);
}
#[test]
#[rustfmt::skip]
fn diff_multi_width() {
let prev = Buffer::with_lines(vec![
"┌Title─┐ ",
"└──────┘ ",
]);
let next = Buffer::with_lines(vec![
"┌称号──┐ ",
"└──────┘ ",
]);
let diff = prev.diff(&next);
assert_eq!(
diff,
vec![
(1, 0, &cell("")),
// Skipped "i"
(3, 0, &cell("")),
// Skipped "l"
(5, 0, &cell("")),
]
);
}
#[test]
fn diff_multi_width_offset() {
let prev = Buffer::with_lines(vec!["┌称号──┐"]);
let next = Buffer::with_lines(vec!["┌─称号─┐"]);
let diff = prev.diff(&next);
assert_eq!(
diff,
vec![(1, 0, &cell("")), (2, 0, &cell("")), (4, 0, &cell("")),]
);
}
#[test]
fn diff_skip() {
let prev = Buffer::with_lines(vec!["123"]);
let mut next = Buffer::with_lines(vec!["456"]);
for i in 1..3 {
next.content[i].set_skip(true);
}
let diff = prev.diff(&next);
assert_eq!(diff, vec![(0, 0, &cell("4"))],);
}
#[test]
fn merge() {
let mut one = Buffer::filled(
Rect {
x: 0,
y: 0,
width: 2,
height: 2,
},
Cell::default().set_symbol("1"),
);
let two = Buffer::filled(
Rect {
x: 0,
y: 2,
width: 2,
height: 2,
},
Cell::default().set_symbol("2"),
);
one.merge(&two);
assert_buffer_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
}
#[test]
fn merge2() {
let mut one = Buffer::filled(
Rect {
x: 2,
y: 2,
width: 2,
height: 2,
},
Cell::default().set_symbol("1"),
);
let two = Buffer::filled(
Rect {
x: 0,
y: 0,
width: 2,
height: 2,
},
Cell::default().set_symbol("2"),
);
one.merge(&two);
assert_buffer_eq!(
one,
Buffer::with_lines(vec!["22 ", "22 ", " 11", " 11"])
);
}
#[test]
fn merge3() {
let mut one = Buffer::filled(
Rect {
x: 3,
y: 3,
width: 2,
height: 2,
},
Cell::default().set_symbol("1"),
);
let two = Buffer::filled(
Rect {
x: 1,
y: 1,
width: 3,
height: 4,
},
Cell::default().set_symbol("2"),
);
one.merge(&two);
let mut merged = Buffer::with_lines(vec!["222 ", "222 ", "2221", "2221"]);
merged.area = Rect {
x: 1,
y: 1,
width: 4,
height: 4,
};
assert_buffer_eq!(one, merged);
}
#[test]
fn merge_skip() {
let mut one = Buffer::filled(
Rect {
x: 0,
y: 0,
width: 2,
height: 2,
},
Cell::default().set_symbol("1"),
);
let two = Buffer::filled(
Rect {
x: 0,
y: 1,
width: 2,
height: 2,
},
Cell::default().set_symbol("2").set_skip(true),
);
one.merge(&two);
let skipped: Vec<bool> = one.content().iter().map(|c| c.skip).collect();
assert_eq!(skipped, vec![false, false, true, true, true, true]);
}
#[test]
fn merge_skip2() {
let mut one = Buffer::filled(
Rect {
x: 0,
y: 0,
width: 2,
height: 2,
},
Cell::default().set_symbol("1").set_skip(true),
);
let two = Buffer::filled(
Rect {
x: 0,
y: 1,
width: 2,
height: 2,
},
Cell::default().set_symbol("2"),
);
one.merge(&two);
let skipped: Vec<bool> = one.content().iter().map(|c| c.skip).collect();
assert_eq!(skipped, vec![true, true, false, false, false, false]);
}
#[test]
fn with_lines_accepts_into_lines() {
use crate::style::Stylize;
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 2));
buf.set_string(0, 0, "foo", Style::new().red());
buf.set_string(0, 1, "bar", Style::new().blue());
assert_eq!(buf, Buffer::with_lines(vec!["foo".red(), "bar".blue()]));
}
}

Some files were not shown because too many files have changed in this diff Show More