Compare commits

...

975 Commits

Author SHA1 Message Date
Josh McKinney
67c0ea243b chore(block): deprecate block::Title (#1372)
`ratatui::widgets::block::Title` is deprecated in favor of using `Line`
to represent titles.
This removes an unnecessary layer of wrapping (string -> Span -> Line ->
Title).

This struct will be removed in a future release of Ratatui (likely
0.31).
For more information see:
<https://github.com/ratatui/ratatui/issues/738>

To update your code:
```rust
Block::new().title(Title::from("foo"));
// becomes any of
Block::new().title("foo");
Block::new().title(Line::from("foo"));

Block::new().title(Title::from("foo").position(Position::TOP));
// becomes any of
Block::new().title_top("foo");
Block::new().title_top(Line::from("foo"));

Block::new().title(Title::from("foo").position(Position::BOTTOM));
// becomes any of
Block::new().title_bottom("foo");
Block::new().title_bottom(Line::from("foo"));
```
2024-09-20 10:21:26 +03:00
Anthony Rodgers
b9653ba05a fix: prevent calender render panic when terminal height is small (#1380)
Fixes: #1379
2024-09-19 06:02:26 -07:00
dependabot[bot]
9875d9facc chore(deps): bump DavidAnson/markdownlint-cli2-action from 16 to 17 (#1376)
Bumps
[DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action)
from 16 to 17.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/davidanson/markdownlint-cli2-action/releases">DavidAnson/markdownlint-cli2-action's
releases</a>.</em></p>
<blockquote>
<h2>Update markdownlint version (markdownlint-cli2 v0.14.0, markdownlint
v0.35.0).</h2>
<p>No release notes provided.</p>
<h2>Update markdownlint version (markdownlint-cli2 v0.13.0, markdownlint
v0.34.0).</h2>
<p>No release notes provided.</p>
<p>Update markdownlint version (markdownlint-cli2 v0.12.1, markdownlint
v0.33.0).</p>
<h2>Update markdownlint version (markdownlint-cli2 v0.11.0, markdownlint
v0.32.1), remove deprecated &quot;command&quot; input.</h2>
<p>No release notes provided.</p>
<h2>Update markdownlint version (markdownlint-cli2 v0.10.0, markdownlint
v0.31.1).</h2>
<p>No release notes provided.</p>
<h2>Update markdownlint version (markdownlint-cli2 v0.9.2, markdownlint
v0.30.0).</h2>
<p>No release notes provided.</p>
<h2>Update markdownlint version (markdownlint-cli2 v0.8.1, markdownlint
v0.29.0), add &quot;config&quot; and &quot;fix&quot; inputs, deprecate
&quot;command&quot; input.</h2>
<p>No release notes provided.</p>
<h2>Update markdownlint version (markdownlint-cli2 v0.7.1, markdownlint
v0.28.2).</h2>
<p>No release notes provided.</p>
<h2>Update markdownlint version (markdownlint-cli2 v0.7.0, markdownlint
v0.28.1), include link to rule information in title of annotations
(clickable in GitHub).</h2>
<p>No release notes provided.</p>
<h2>Update markdownlint version (markdownlint-cli2 v0.6.0, markdownlint
v0.27.0).</h2>
<p>No release notes provided.</p>
<h2>Update markdownlint version (markdownlint-cli2 v0.5.1, markdownlint
v0.26.2).</h2>
<p>No release notes provided.</p>
<h2>Update markdownlint version (markdownlint-cli2 v0.4.0, markdownlint
v0.25.1)</h2>
<p>No release notes provided.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="db43aef879"><code>db43aef</code></a>
Update to version 17.0.0.</li>
<li><a
href="c0decc52d0"><code>c0decc5</code></a>
Bump <code>@​stylistic/eslint-plugin</code> from 2.7.2 to 2.8.0</li>
<li><a
href="dd2171bb17"><code>dd2171b</code></a>
Bump eslint from 9.9.1 to 9.10.0</li>
<li><a
href="85b2286968"><code>85b2286</code></a>
Bump <code>@​eslint/js</code> from 9.9.1 to 9.10.0</li>
<li><a
href="95aa6ed6ed"><code>95aa6ed</code></a>
Freshen generated index.js file.</li>
<li><a
href="476aead54e"><code>476aead</code></a>
Bump markdownlint-cli2 from 0.13.0 to 0.14.0</li>
<li><a
href="da0291d977"><code>da0291d</code></a>
Freshen generated index.js file.</li>
<li><a
href="235535bdb7"><code>235535b</code></a>
Add <code>@​stylistic/eslint-plugin</code> to ESLint configuration.</li>
<li><a
href="20384985f1"><code>2038498</code></a>
Bump eslint from 9.9.0 to 9.9.1</li>
<li><a
href="ea9d2c1e2e"><code>ea9d2c1</code></a>
Bump <code>@​eslint/js</code> from 9.9.0 to 9.9.1</li>
<li>Additional commits viewable in <a
href="https://github.com/davidanson/markdownlint-cli2-action/compare/v16...v17">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=DavidAnson/markdownlint-cli2-action&package-manager=github_actions&previous-version=16&new-version=17)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-16 09:24:38 -07:00
Josh McKinney
b88717b65f docs(constraint): add note about percentages (#1368)
Co-authored-by: Orhun Parmaksız <orhun@archlinux.org>
2024-09-13 14:42:52 +03:00
Vitalii Kryvenko
5635b930c7 ci: add cargo-machete and remove unused deps (#1362)
https://github.com/bnjbvr/cargo-machete
2024-09-08 18:54:33 -07:00
Hossein Nedaee
870bc6a64a docs: use Frame::area() instead of size() in examples (#1361)
`Frame::size()` is deprecated
2024-09-08 13:20:59 -07:00
FujiApple
da821b431e fix: clippy lints from rust 1.81.0 (#1356) 2024-09-06 22:54:36 -07:00
Patryk Wychowaniec
68886d1787 fix: add unstable-backend-writer feature (#1352)
https://github.com/ratatui/ratatui/pull/991 created a new unstable
feature, but forgot to add it to Cargo.toml, making it impossible to use
on newer versions of rustc - this commit fixes it.
2024-09-06 15:33:14 -07:00
Patryk Wychowaniec
0f48239778 fix(terminal): resize() now resizes fixed viewports (#1353)
`Terminal::resize()` on a fixed viewport used to do nothing due to
an accidentally shadowed variable. This now works as intended.
2024-09-04 12:28:30 -07:00
Valentin271
b13e2f9473 docs(backend): added link to stdio FAQ (#1349) 2024-09-01 15:30:13 -07:00
Orhun Parmaksız
c777beb658 chore(ci): bump git-cliff-action to v4 (#1350)
See: https://github.com/orhun/git-cliff-action/releases/tag/v4.0.0
2024-09-01 16:23:35 +02:00
Mo
20c88aaa5b refactor: Avoid unneeded allocations (#1345) 2024-08-26 13:59:30 -07:00
Orhun Parmaksız
e02947be61 style(example): update panic message in minimal template (#1344) 2024-08-25 04:45:34 -07:00
Orhun Parmaksız
3a90e2a761 chore(release): prepare for 0.28.1 (#1343)
🧀 

The current release steps in reference to #1337

- Bump version in `Cargo.toml`
- `git cliff -u -p CHANGELOG.md -t v0.28.1`
- Merge the PR
- `git tag v0.28.1`
- `git push origin v0.28.1`

We can probably automate away most of these with `release-plz` when it
fully supports `git-cliff`'s GitHub integration.
2024-08-25 12:23:26 +03:00
Orhun Parmaksız
65da535745 chore(ci): update release strategy (#1337)
closes #1232 

Now we can trigger point releases by pushing a tag (follow the
instructions in `RELEASE.md`). This will create a release with generated
changelog.

There is still a lack of automation (e.g. updating `CHANGELOG.md`), but
this PR is a good start towards improving that.
2024-08-25 11:02:29 +03:00
Tayfun Bocek
9ed85fd1dd docs(table): fix incorrect backticks in TableState docs (#1342) 2024-08-24 14:26:37 -07:00
Neal Fachan
aed60b9839 fix(terminal): Terminal::insert_before would crash when called while the viewport filled the screen (#1329)
Reimplement Terminal::insert_before. The previous implementation would
insert the new lines in chunks into the area between the top of the
screen and the top of the (new) viewport. If the viewport filled the
screen, there would be no area in which to insert lines, and the
function would crash.

The new implementation uses as much of the screen as it needs to, all
the way up to using the whole screen.

This commit:
- adds a scrollback buffer to the `TestBackend` so that tests can
inspect and assert the state of the scrollback buffer in addition to the
screen
- adds functions to `TestBackend` to assert the state of the scrollback
- adds and updates `TestBackend` tests to test the behavior of the
scrollback and the new asserting functions
- reimplements `Terminal::insert_before`, including adding two new
helper functions `Terminal::draw_lines` and `Terminal::scroll_up`.
- updates the documentation for `Terminal::insert_before` to clarify
some of the edge cases
- updates terminal tests to assert the state of the scrollback buffer
- adds a new test for the condition that causes the bug
- adds a conversion constructor `Cell::from(char)`

Fixes: https://github.com/ratatui/ratatui/issues/999
2024-08-23 15:27:54 -07:00
Josh McKinney
3631b34f53 docs(examples): add widget implementation example (#1147)
This new example documents the various ways to implement widgets in
Ratatui. It demonstrates how to implement the `Widget` trait on a type,
a reference, and a mutable reference. It also shows how to use the
`WidgetRef` trait to render boxed widgets.
2024-08-23 14:30:23 -07:00
Mo
0d5f3c091f test: Avoid unneeded allocations in assertions (#1335)
A vector can be compared to an array.
2024-08-22 09:14:16 -07:00
Josh McKinney
ed51c4b342 feat(terminal): Add ratatui::init() and restore() methods (#1289)
These are simple opinionated methods for creating a terminal that is
useful to use in most apps. The new init method creates a crossterm
backend writing to stdout, enables raw mode, enters the alternate
screen, and sets a panic handler that restores the terminal on panic.

A minimal hello world now looks a bit like:

```rust
use ratatui::{
    crossterm::event::{self, Event},
    text::Text,
    Frame,
};

fn main() {
    let mut terminal = ratatui::init();
    loop {
        terminal
            .draw(|frame: &mut Frame| frame.render_widget(Text::raw("Hello World!"), frame.area()))
            .expect("Failed to draw");
        if matches!(event::read().expect("failed to read event"), Event::Key(_)) {
            break;
        }
    }
    ratatui::restore();
}
```

A type alias `DefaultTerminal` is added to represent this terminal
type and to simplify any cases where applications need to pass this
terminal around. It is equivalent to:
`Terminal<CrosstermBackend<Stdout>>`

We also added `ratatui::try_init()` and `try_restore()`, for situations
where you might want to handle initialization errors yourself instead
of letting the panic handler fire and cleanup. Simple Apps should
prefer the `init` and `restore` functions over these functions.

Corresponding functions to allow passing a `TerminalOptions` with
a `Viewport` (e.g. inline, fixed) are also available
(`init_with_options`,
and `try_init_with_options`).

The existing code to create a backend and terminal will remain and
is not deprecated by this approach. This just provides a simple one
line initialization using the common options.

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-08-22 15:16:35 +03:00
Josh McKinney
23516bce76 chore: rename ratatui-org to ratatui (#1334)
All urls updated to point at https://github.com/ratatui

To update your repository remotes, you can run the following commands:

```shell
git remote set-url origin https://github.com/ratatui/ratatui
```
2024-08-21 11:35:08 -07:00
Matt Armstrong
6d1bd99544 docs: minor grammar fixes (#1330) 2024-08-17 16:17:42 -07:00
Neal Fachan
2fb0b8a741 fix: fix u16 overflow in Terminal::insert_before. (#1323)
If the amount of characters in the screen above the viewport was greater
than u16::MAX, a multiplication would overflow. The multiply was used to
compute the maximum chunk size. The fix is to just do the multiplication
as a usize and also do the subsequent division as a usize.

There is currently another outstanding issue that limits the amount of
characters that can be inserted when calling Terminal::insert_before to
u16::MAX. However, this bug can still occur even if the viewport and the
amount of characters being inserted are both less than u16::MAX, since
it's dependant on how large the screen is above the viewport.

Fixes #1322
2024-08-13 21:06:49 -07:00
Josh McKinney
0256269a7f build: simplify Windows build (#1317)
Termion is not supported on Windows, so we need to avoid building it.

Adds a conditional dependency to the Cargo.toml file to only include
termion when the target is not Windows. This allows contributors to
build using the `--all-features` flag on Windows rather than needing
to specify the features individually.
2024-08-13 10:09:46 -07:00
Lucas Pickering
fdd5d8c092 fix(text): remove trailing newline from single-line Display trait impl (#1320) 2024-08-11 20:30:36 -07:00
EdJoPaTo
8b624f5952 chore(maintainers): remove EdJoPaTo (#1314) 2024-08-11 20:27:11 -07:00
Josh McKinney
57d8b742e5 chore(ci): use cargo-docs-rs to lint docs (#1318) 2024-08-11 20:09:57 -07:00
Josh McKinney
d5477b50d5 docs(examples): use ratatui::crossterm in examples (#1315) 2024-08-10 17:43:13 -07:00
montmorillonite
730dfd4940 docs(examples): show line gauge in demo example (#1309) 2024-08-07 20:25:43 -07:00
EdJoPaTo
097ee86e39 docs: remove superfluous doc(inline) (#1310)
It's no longer needed since #1260
2024-08-07 20:25:07 -07:00
Jack Wills
3fdb5e8987 docs: fix typo in terminal.rs (#1313) 2024-08-07 19:34:21 -07:00
Orhun Parmaksız
ec88bb81e5 chore(release): prepare for 0.28.0 (#1295)
🧀
2024-08-07 14:56:01 +03:00
Josh McKinney
f04bf855cb perf: add buffer benchmarks (#1303) 2024-08-06 23:05:36 -07:00
Alex Saveau
4753b7241b perf(reflow): eliminate most WordWrapper allocations (#1239)
On large paragraphs (~1MB), this saves hundreds of thousands of
allocations.

TL;DR: reuse as much memory as possible across `next_line` calls.
Instead of allocating new buffers each time, allocate the buffers once
and clear them before reuse.

Signed-off-by: Alex Saveau <saveau.alexandre@gmail.com>
2024-08-06 20:49:05 -07:00
Josh McKinney
36fa3c11c1 chore(deps): bump crossterm to 0.28.1 (#1304)
https://github.com/crossterm-rs/crossterm/blob/master/CHANGELOG.md\#version-0281
2024-08-07 00:09:38 +03:00
Josh McKinney
69e8ed7db8 chore(deps): remove anyhow from dev dependencies (#1305)
Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-08-06 23:17:11 +03:00
Josh McKinney
5f7a7fbe19 docs(examples): update barcharts gifs (#1306) 2024-08-06 23:09:40 +03:00
Josh McKinney
e6d2e04bcf perf: move benchmarks into a single benchmark harness (#1302)
Consolidates the benchmarks into a single executable rather than having
to create a new cargo.toml setting per and makes it easier to rearrange
these when adding new benchmarks.
2024-08-06 05:31:13 -07:00
Josh McKinney
45fcab7497 chore: add rect::rows benchmark (#1301) 2024-08-06 05:30:07 -07:00
EdJoPaTo
1b9bdd425c docs(contributing): fix minor issues (#1300)
Co-authored-by: DeflateAwning <11021263+DeflateAwning@users.noreply.github.com>
2024-08-06 04:12:08 -07:00
EdJoPaTo
c68ee6c64a feat!: add get/set_cursor_position() methods to Terminal and Backend (#1284)
The new methods return/accept `Into<Position>` which can be either a Position or a (u16, u16) tuple.

```rust
backend.set_cursor_position(Position { x: 0, y: 20 })?;
let position = backend.get_cursor_position()?;
terminal.set_cursor_position((0, 20))?;
let position = terminal.set_cursor_position()?;
```
2024-08-06 04:10:28 -07:00
EdJoPaTo
afe15349c8 feat(chart)!: accept IntoIterator for axis labels (#1283)
BREAKING CHANGES: #1273 is already breaking and this only advances the
already breaking part
2024-08-06 11:39:44 +02:00
Josh McKinney
fe4eeab676 docs(examples): simplify the barchart example (#1079)
The `barchart` example has been split into two examples: `barchart` and
`barchart-grouped`. The `barchart` example now shows a simple barchart
with random data, while the `barchart-grouped` example shows a grouped
barchart with fake revenue data.

This simplifies the examples a bit so they don't cover too much at once.

- Simplify the rendering functions
- Fix several clippy lints that were marked as allowed

---------

Co-authored-by: EdJoPaTo <rfc-conform-git-commit-email@funny-long-domain-label-everyone-hates-as-it-is-too-long.edjopato.de>
2024-08-06 01:10:58 -07:00
Josh McKinney
a23ecd9b45 feat(buffer): add Buffer::cell, cell_mut and index implementations (#1084)
Code which previously called `buf.get(x, y)` or `buf.get_mut(x, y)`
should now use index operators, or be transitioned to `buff.cell()` or
`buf.cell_mut()` for safe access that avoids panics by returning
`Option<&Cell>` and `Option<&mut Cell>`.

The new methods accept `Into<Position>` instead of `x` and `y`
coordinates, which makes them more ergonomic to use.

```rust
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));

let cell = buf[(0, 0)];
let cell = buf[Position::new(0, 0)];

let symbol = buf.cell((0, 0)).map(|cell| cell.symbol());
let symbol = buf.cell(Position::new(0, 0)).map(|cell| cell.symbol());

buf[(0, 0)].set_symbol("🐀");
buf[Position::new(0, 0)].set_symbol("🐀");

buf.cell_mut((0, 0)).map(|cell| cell.set_symbol("🐀"));
buf.cell_mut(Position::new(0, 0)).map(|cell| cell.set_symbol("🐀"));
```

The existing `get()` and `get_mut()` methods are marked as deprecated.
These are fairly widely used and we will leave these methods around on
the buffer for a longer time than our normal deprecation approach (2
major release)

Addresses part of: https://github.com/ratatui-org/ratatui/issues/1011

---------

Co-authored-by: EdJoPaTo <rfc-conform-git-commit-email@funny-long-domain-label-everyone-hates-as-it-is-too-long.edjopato.de>
2024-08-06 00:40:47 -07:00
EdJoPaTo
bb71e5ffd4 docs(readme): remove MSRV (#1266)
This notice was useful when the `Cargo.toml` had no standardized field
for this. Now it's easier to look it up in the `Cargo.toml` and it's
also a single point of truth. Updating the README was overlooked for
quite some time so it's better to just omit it rather than having
something wrong that will be forgotten again in the future.
2024-08-05 22:04:48 -07:00
Orhun Parmaksız
f2fa1ae9aa docs(breaking-changes): add missing code block (#1291) 2024-08-05 20:25:01 -07:00
Orhun Parmaksız
f687af7c0d docs(breaking-changes): mention removed lifetime of ToText trait (#1292) 2024-08-05 20:18:58 -07:00
EdJoPaTo
f97e07c08a feat(frame): replace Frame::size() with Frame::area() (#1293)
Area is the more correct term for the result of this method.
The Frame::size() method is marked as deprecated and will be
removed around Ratatui version 0.30 or later.

Fixes: https://github.com/ratatui-org/ratatui/pull/1254#issuecomment-2268061409
2024-08-05 20:15:14 -07:00
EdJoPaTo
bb68bc6968 refactor(backend)!: return Size from Backend::size instead of Rect (#1254)
The `Backend::size` method returns a `Size` instead of a `Rect`.
There is no need for the position here as it was always 0,0.
2024-08-05 17:36:50 -07:00
dependabot[bot]
ffeb8e46b9 chore(deps): update rstest requirement from 0.21.0 to 0.22.0 (#1297)
Updates the requirements on [rstest](https://github.com/la10736/rstest)
to permit the latest version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/la10736/rstest/releases">rstest's
releases</a>.</em></p>
<blockquote>
<h2>Version 0.22.0</h2>
<p>Destructuring input data</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/la10736/rstest/blob/master/CHANGELOG.md">rstest's
changelog</a>.</em></p>
<blockquote>
<h2>[0.22.0] 2024/8/4</h2>
<h3>Changed</h3>
<ul>
<li>Now it's possible destructuring input values both for cases, values
and fixtures. See <a
href="https://redirect.github.com/la10736/rstest/issues/231">#231</a>
for details</li>
</ul>
<h3>Add</h3>
<ul>
<li>Implemented <code>#[ignore]</code> attribute to ignore test
parameters during fixtures resolution/injection. See <a
href="https://redirect.github.com/la10736/rstest/issues/228">#228</a>
for details</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Lot of typo in code</li>
</ul>
<h2>[0.21.0] 2024/6/1</h2>
<h3>Changed</h3>
<ul>
<li>Add feature <code>crate-name</code> enabled by default to opt-in
crate rename
support. See <a
href="https://redirect.github.com/la10736/rstest/issues/258">#258</a></li>
</ul>
<h2>[0.20.0] 2024/5/30</h2>
<h3>Add</h3>
<ul>
<li>Implemented <code>#[by_ref]</code> attribute to take get a local
lifetime for test arguments.
See <a
href="https://redirect.github.com/la10736/rstest/issues/241">#241</a>
for more details. Thanks to
<a href="https://github.com/narpfel"><code>@​narpfel</code></a> for
suggesting it and useful discussions.</li>
<li>Support for import <code>rstest</code> with another name. See <a
href="https://redirect.github.com/la10736/rstest/issues/221">#221</a></li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Don't remove Lifetimes from test function if any. See <a
href="https://redirect.github.com/la10736/rstest/issues/230">#230</a>
<a href="https://redirect.github.com/la10736/rstest/issues/241">#241</a>
for more details.</li>
<li><a
href="https://doc.rust-lang.org/std/path/struct.PathBuf.html"><code>PathBuf</code></a>
does no longer need to be
in scope when using <code>#[files]</code> (see <a
href="https://redirect.github.com/la10736/rstest/pull/242">#242</a>)</li>
<li><code>#[from(now::accept::also::path::for::fixture)]</code> See <a
href="https://redirect.github.com/la10736/rstest/issues/246">#246</a>
for more details</li>
</ul>
<h2>[0.19.0] 2024/4/9</h2>
<h3>Changed</h3>
<ul>
<li>Defined <code>rust-version</code> for each crate (see <a
href="https://redirect.github.com/la10736/rstest/issues/227">#227</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li><code>#[once]</code> fixtures now require the returned type to be
<a
href="https://doc.rust-lang.org/std/marker/trait.Sync.html"><code>Sync</code></a>
to prevent UB
when tests are executed in parallel. (see <a
href="https://redirect.github.com/la10736/rstest/issues/235">#235</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="62134281cf"><code>6213428</code></a>
Prepare 0.22.0</li>
<li><a
href="16591fdcef"><code>16591fd</code></a>
Make clippy happy</li>
<li><a
href="d40e785d74"><code>d40e785</code></a>
Merge pull request <a
href="https://redirect.github.com/la10736/rstest/issues/269">#269</a>
from la10736/fix_typos</li>
<li><a
href="9110f0cff7"><code>9110f0c</code></a>
Fix typo</li>
<li><a
href="696eaf63c1"><code>696eaf6</code></a>
Merge pull request <a
href="https://redirect.github.com/la10736/rstest/issues/268">#268</a>
from la10736/arg_destruct</li>
<li><a
href="39490761ca"><code>3949076</code></a>
Fixed warning in beta</li>
<li><a
href="d35ade2521"><code>d35ade2</code></a>
Docs and make clippy happy</li>
<li><a
href="40087a7aa3"><code>40087a7</code></a>
Implementation and integration tests</li>
<li><a
href="fcf732dc34"><code>fcf732d</code></a>
Merge pull request <a
href="https://redirect.github.com/la10736/rstest/issues/267">#267</a>
from marcobacis/ignore_parameter</li>
<li><a
href="cf9dd0bf51"><code>cf9dd0b</code></a>
update docs, simplified unit test</li>
<li>Additional commits viewable in <a
href="https://github.com/la10736/rstest/compare/v0.21.0...v0.22.0">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 17:31:01 -07:00
Josh McKinney
2fd5ae64bf docs(widgets): document stability of WidgetRef (#1288)
Addresses some confusion about when to implement `WidgetRef` vs `impl
Widget for &W`. Notes the stability rationale and links to an issue that
helps explain the context of where we're at in working this out.
2024-08-05 17:29:14 -07:00
Orhun Parmaksız
82b70fd329 chore(ci): integrate cargo-semver-checks (#1166)
>
[`cargo-semver-checks`](https://github.com/obi1kenobi/cargo-semver-checks):
Lint your crate API changes for semver violations.
2024-08-05 19:28:38 +03:00
dependabot[bot]
94328a2977 chore(deps): bump EmbarkStudios/cargo-deny-action from 1 to 2 (#1296)
Bumps
[EmbarkStudios/cargo-deny-action](https://github.com/embarkstudios/cargo-deny-action)
from 1 to 2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/embarkstudios/cargo-deny-action/releases">EmbarkStudios/cargo-deny-action's
releases</a>.</em></p>
<blockquote>
<h2>Release 2.0.1 - cargo-deny 0.16.1</h2>
<h3>Fixed</h3>
<ul>
<li><a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/pull/691">PR#691</a>
fixed an issue where workspace dependencies that used the current dir
'.' path component would incorrectly trigger the
<code>unused-workspace-dependency</code> lint.</li>
</ul>
<h2>Release 2.0.0 - cargo-deny 0.16.0</h2>
<h2><code>Action</code></h2>
<h3>Added</h3>
<ul>
<li><a
href="https://redirect.github.com/EmbarkStudios/cargo-deny-action/pull/78">PR#78</a>
added SSH support, thanks <a
href="https://github.com/nagua"><code>@​nagua</code></a>!</li>
</ul>
<h3>Changed</h3>
<ul>
<li>This release includes breaking changes in cargo-deny, so this
release begins the <code>v2</code> tag, using <code>v1</code> will be
stable but not follow future <code>cargo-deny</code> releases.</li>
</ul>
<h2><code>cargo-deny</code></h2>
<h3>Removed</h3>
<ul>
<li><a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/pull/681">PR#681</a>
finished the deprecation introduced in <a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/pull/611">PR#611</a>,
making the usage of the deprecated fields into errors.</li>
</ul>
<h4><code>[advisories]</code></h4>
<p>The following fields have all been removed in favor of denying all
advisories by default. To ignore an advisory the <a
href="https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html#the-ignore-field-optional"><code>ignore</code></a>
field can be used as before.</p>
<ul>
<li><code>vulnerability</code> - Vulnerability advisories are now
<code>deny</code> by default</li>
<li><code>unmaintained</code> - Unmaintained advisories are now
<code>deny</code> by default</li>
<li><code>unsound</code> - Unsound advisories are now <code>deny</code>
by default</li>
<li><code>notice</code> - Notice advisories are now <code>deny</code> by
default</li>
<li><code>severity-threshold</code> - The severity of vulnerabilities is
now irrelevant</li>
</ul>
<h4><code>[licenses]</code></h4>
<p>The following fields have all been removed in favor of denying all
licenses that are not explicitly allowed via either <a
href="https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html#the-allow-field-optional"><code>allow</code></a>
or <a
href="https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html#the-exceptions-field-optional"><code>exceptions</code></a>.</p>
<ul>
<li><code>unlicensed</code> - Crates whose license(s) cannot be
confidently determined are now always errors. The <a
href="https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html#the-clarify-field-optional"><code>clarify</code></a>
field can be used to help cargo-deny determine the license.</li>
<li><code>allow-osi-fsf-free</code> - The OSI/FSF Free attributes are
now irrelevant, only whether it is explicitly allowed.</li>
<li><code>copyleft</code> - The copyleft attribute is now irrelevant,
only whether it is explicitly allowed.</li>
<li><code>default</code> - The default is now <code>deny</code>.</li>
<li><code>deny</code> - All licenses are now denied by default, this
field added nothing.</li>
</ul>
<h3>Changed</h3>
<ul>
<li><a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/pull/685">PR#685</a>
follows up on <a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/pull/673">PR#673</a>,
moving the fields that were added to their own separate <a
href="https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html#the-workspace-dependencies-field-optional"><code>bans.workspace-dependencies</code></a>
section. This is an unannounced breaking change but is fairly minor and
0.15.0 was never released on github actions so the amount of people
affected by this will be (hopefully) small. This also makes the
workspace duplicate detection off by default since the field is
optional, <em>but</em> makes it so that if not specified workspace
duplicates are now <code>deny</code> instead of <code>warn</code>.</li>
</ul>
<h3>Fixed</h3>
<ul>
<li><a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/pull/685">PR#685</a>
resolved <a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/issues/682">#682</a>
by adding the <code>include-path-dependencies</code> field, allowing
path dependencies to be ignored if it is <code>false</code>.</li>
</ul>
<h2>Release 1.6.3 - cargo-deny 0.14.21</h2>
<h3>Fixed</h3>
<ul>
<li><a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/pull/643">PR#643</a>
resolved <a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/issues/629">#629</a>
by making the hosted git (github, gitlab, bitbucket) org/user name
comparison case-insensitive. Thanks <a
href="https://github.com/pmnlla"><code>@​pmnlla</code></a>!</li>
<li><a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/pull/649">PR#649</a>
fixed an issue where depending on the same crate multiple times by using
different <code>cfg()/triple</code> targets could cause features to be
resolved incorrectly and thus crates to be not pulled into the graph
used for checking.</li>
</ul>
<h2>[0.14.20] - 2024-03-23</h2>
<h3>Fixed</h3>
<ul>
<li><a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/pull/642">PR#642</a>
resolved <a
href="https://redirect.github.com/EmbarkStudios/cargo-deny/issues/641">#641</a>
by pinning <code>gix-transport</code> (and its unique dependencies) to
0.41.2 as a workaround for <code>cargo install</code> not using the
lockfile. See <a
href="https://redirect.github.com/Byron/gitoxide/issues/1328">this
issue</a> for more information.</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="8371184bd1"><code>8371184</code></a>
Bump to 0.16.1</li>
<li><a
href="08954043da"><code>0895404</code></a>
Move to v2 tag</li>
<li><a
href="10d8902cf9"><code>10d8902</code></a>
Bump to 0.16.0</li>
<li><a
href="d425dbf412"><code>d425dbf</code></a>
Update .gitignore</li>
<li><a
href="53bface5b1"><code>53bface</code></a>
Update image base</li>
<li><a
href="3f8dc3eed7"><code>3f8dc3e</code></a>
Add ability to fetch git dependecies in cargo via ssh (<a
href="https://redirect.github.com/embarkstudios/cargo-deny-action/issues/78">#78</a>)</li>
<li>See full diff in <a
href="https://github.com/embarkstudios/cargo-deny-action/compare/v1...v2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=EmbarkStudios/cargo-deny-action&package-manager=github_actions&previous-version=1&new-version=2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 19:18:21 +03:00
Orhun Parmaksız
d468463fc6 docs(breaking-changes): fix the PR link (#1294) 2024-08-05 15:02:26 +03:00
Josh McKinney
6e7b4e4d55 docs(examples): add async example (#1248)
This example demonstrates how to use Ratatui with widgets that fetch
data asynchronously. It uses the `octocrab` crate to fetch a list of
pull requests from the GitHub API. You will need an environment
variable named `GITHUB_TOKEN` with a valid GitHub personal access
token. The token does not need any special permissions.

Co-authored-by: Dheepak Krishnamurthy <me@kdheepak.com>
2024-08-04 22:26:56 -07:00
EdJoPaTo
864cd9ffef fix(TestBackend): prevent area mismatch (#1252)
Removes the height and width fields from TestBackend, which can get
out of sync with the Buffer, which currently clamps to 255,255.

This changes the `TestBackend` serde representation. It should be
possible to read older data, but data generated after this change
can't be read by older versions.
2024-08-04 18:34:08 -07:00
EdJoPaTo
c08b522d34 fix(chart): allow removing all the axis labels (#1282)
`axis.labels(vec![])` removes all the labels correctly.

This makes calling axis.labels with an empty Vec the equivalent
of not calling axis.labels. It's likely that this is never used, but it
prevents weird cases by removing the mix-up of `Option::None`
and `Vec::is_empty`, and simplifies the implementation code.
2024-08-04 16:41:20 -07:00
Josh McKinney
716c93136e docs: document crossterm breaking change (#1281) 2024-08-04 15:15:18 -07:00
Josh McKinney
5eeb1ccbc4 docs(github): Create CODE_OF_CONDUCT.md (#1279) 2024-08-04 17:26:02 +03:00
Josh McKinney
f77503050f docs: update main lib.rs / README examples (#1280) 2024-08-04 05:09:28 -07:00
dependabot[bot]
cd0d31c2dc chore(deps): update crossterm requirement from 0.27 to 0.28 (#1278)
Updates the requirements on
[crossterm](https://github.com/crossterm-rs/crossterm) to permit the
latest version.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/crossterm-rs/crossterm/blob/master/CHANGELOG.md">crossterm's
changelog</a>.</em></p>
<blockquote>
<h1>Version 0.28.1</h1>
<h2>Fixed 🐛</h2>
<ul>
<li>Fix broken build on linux when using <code>use-dev-tty</code> with
(<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/906">#906</a>)</li>
</ul>
<h2>Breaking ⚠️</h2>
<ul>
<li>Fix desync with mio and signalhook between repo and published crate.
(upgrade to mio 1.0)</li>
</ul>
<h1>Version 0.28</h1>
<h2>Added </h2>
<ul>
<li>Capture double click mouse events on windows (<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/826">#826</a>)</li>
<li>(De)serialize Reset color (<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/824">#824</a>)</li>
<li>Add functions to allow constructing <code>Attributes</code> in a
const context (<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/817">#817</a>)</li>
<li>Implement <code>Display</code> for <code>KeyCode</code> and
<code>KeyModifiers</code> (<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/862">#862</a>)</li>
</ul>
<h2>Changed ⚙️</h2>
<ul>
<li>Use Rustix by default instead of libc. Libc can be re-enabled if
necessary with the <code>libc</code> feature flag (<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/892">#892</a>)</li>
<li><code>FileDesc</code> now requires a lifetime annotation.</li>
<li>Improve available color detection (<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/885">#885</a>)</li>
<li>Speed up <code>SetColors</code> by ~15-25% (<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/879">#879</a>)</li>
<li>Remove unsafe and unnecessary size argument from
<code>FileDesc::read()</code> (<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/821">#821</a>)</li>
</ul>
<h2>Breaking ⚠️</h2>
<ul>
<li>Fix duplicate bit masks for caps lock and num lock (<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/863">#863</a>).
This breaks serialization of <code>KeyEventState</code></li>
</ul>
<h1>Version 0.27.1</h1>
<h2>Added </h2>
<ul>
<li>Add support for (de)serializing <code>Reset</code>
<code>Color</code></li>
</ul>
<h1>Version 0.27</h1>
<h2>Added </h2>
<ul>
<li>Add <code>NO_COLOR</code> support (<a
href="https://no-color.org/">https://no-color.org/</a>)</li>
<li>Add option to force overwrite <code>NO_COLOR</code> (<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/802">#802</a>)</li>
<li>Add support for scroll left/right events on windows and unix systems
(<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/788">#788</a>).</li>
<li>Add <code>window_size</code> function to fetch pixel width/height of
screen for more sophisticated rendering in terminals.</li>
<li>Add support for deserializing hex color strings to
<code>Color</code> e.g #fffff.</li>
</ul>
<h2>Changed ⚙️</h2>
<ul>
<li>Make the events module an optional feature <code>events</code> (to
make crossterm more lightweight) (<a
href="https://redirect.github.com/crossterm-rs/crossterm/issues/776">#776</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/crossterm-rs/crossterm/commits">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-04 03:34:38 -07:00
EdJoPaTo
41a910004d chore(github): use the GitHub organization team as codeowners (#1081)
Use GitHub organization team in CODEOWNERS and create MAINTAINERS.md
2024-08-04 11:28:54 +03:00
Josh McKinney
8433d0958b docs: update demo image (#1276)
Follow up to https://github.com/ratatui-org/ratatui/pull/1203
2024-08-04 11:06:37 +03:00
Josh McKinney
8d4a1026ab feat(barchart)!: allow axes to accept Lines (#1273)
Fixes: https://github.com/ratatui-org/ratatui/issues/1272
2024-08-03 16:16:57 -07:00
Josh McKinney
edc2af9822 chore: replace big_text with hardcoded logo (#1203)
big_text.rs was a copy of the code from tui-big-text and was getting
gradually out of sync with the original crate. It was also rendering
something a bit different than the Ratatui logo. This commit replaces
the big_text.rs file with a much smaller string representation of the
Ratatui logo.

![demo2](https://raw.githubusercontent.com/ratatui-org/ratatui/images/examples/demo2-destroy.gif)
2024-08-03 16:08:59 -07:00
Josh McKinney
a80a8a6a47 style(format): lint markdown (#1131)
- **chore: Fix line endings for changelog**
- **chore: cleanup markdown lints**
- **ci: add Markdown linter**
- **build: add markdown lint to the makefile**

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-08-03 20:26:04 +03:00
Alex Saveau
29c8c84fd0 fix: ignore newlines in Span's Display impl (#1270) 2024-08-02 22:54:48 -07:00
Josh McKinney
c2d38509b4 chore: use LF line endings for CHANGELOG.md instead of CRLF (#1269) 2024-08-02 21:03:04 -07:00
josueBarretogit
b70cd03c02 feat: add ListState / TableState scroll_down_by() / scroll_up_by() methods (#1267)
Implement new methods `scroll_down_by(u16)` and `scroll_up_by(u16)` for
both `Liststate` and `Tablestate`.

Closes: #1207
2024-08-02 19:09:26 -07:00
EdJoPaTo
476ac87c99 ci: split up lint job (#1264)
This helps with identifying what failed right from the title. Also steps
after a failing one are now always executed.

Also shortens the steps a bit by removing obvious names.
2024-08-02 17:31:18 -07:00
EdJoPaTo
8857037bff docs(terminal): fix imports (#1263) 2024-08-02 21:37:05 +03:00
EdJoPaTo
e707ff11d1 refactor: internally use Position struct (#1256) 2024-08-02 20:55:41 +03:00
EdJoPaTo
a9fe4284ac chore: update cargo-deny config (#1265)
Update `cargo-deny` config (noticed in
https://github.com/ratatui-org/ratatui/pull/1263#pullrequestreview-2215488414)

See https://github.com/EmbarkStudios/cargo-deny/pull/611
2024-08-02 20:53:57 +03:00
EdJoPaTo
ffc4300558 chore: remove executable flag for rs files (#1262) 2024-08-02 05:11:23 -07:00
Josh McKinney
84cb16483a fix(terminal)!: make terminal module private (#1260)
This is a simplification of the public API that is helpful for new users
that are not familiar with how rust re-exports work, and helps avoid
clashes with other modules in the backends that are named terminal.

BREAKING CHANGE: The `terminal` module is now private and can not be
used directly. The types under this module are exported from the root of
the crate.

```diff
- use ratatui::terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
+ use ratatui::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
```

Fixes: https://github.com/ratatui-org/ratatui/issues/1210
2024-08-02 04:18:00 -07:00
EdJoPaTo
5b89bd04a8 feat(layout): add Size::ZERO and Position::ORIGIN constants (#1253) 2024-08-02 03:56:39 -07:00
EdJoPaTo
32d0695cc2 test(buffer): ensure emojis are rendered (#1258) 2024-08-02 11:06:53 +02:00
Alex Saveau
cd93547db8 fix: remove unnecessary synchronization in layout cache (#1245)
Layout::init_cache no longer returns bool and takes a NonZeroUsize instead of usize

The cache is a thread-local, so doesn't make much sense to require
synchronized initialization.
2024-08-01 23:06:49 -07:00
Orhun Parmaksız
c245c13cc1 chore(ci): onboard bencher for tracking benchmarks (#1174)
https://bencher.dev/console/projects/ratatui-org

Closes: #1092
2024-08-01 22:35:51 -07:00
EdJoPaTo
b2aa843b31 feat(layout): enable serde for Margin, Position, Rect, Size (#1255) 2024-08-01 20:51:14 -07:00
EdJoPaTo
3ca920e881 fix(span): prevent panic on rendering out of y bounds (#1257) 2024-08-01 20:17:42 -07:00
Josh McKinney
b344f95b7c fix: only apply style to first line when rendering a Line (#1247)
A `Line` widget should only apply its style to the first line when
rendering and not the entire area. This is because the `Line` widget
should only render a single line of text. This commit fixes the issue by
clamping the area to a single line before rendering the text.
2024-07-27 17:23:21 -07:00
Josh McKinney
b304bb99bd chore(changelog): fixup git-cliff formatting (#1214)
- Replaced backticks surrounding body with blockquotes. This formats
  significantly better.
- Added a few postprocessors to remove junk like the PR template,
  horizontal lines and some trailing whitespace

---

Note: there is extra non-automatically generated stuff in the changelog
that this would remove - the changes to CHANGELOG.md should not be
merged as-is, and this is worth waiting for @orhun to check out.

Compare:
5e7fbe8c32/CHANGELOG.md
To: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-07-27 11:39:00 +03:00
Tayfun Bocek
be3eb75ea5 perf(table): avoid extra allocations when rendering Table (#1242)
When rendering a `Table` the `Text` stored inside of a `Cell` gets
cloned before rendering. This removes the clone and uses `WidgetRef`
instead, saving us from allocating a `Vec<Line<'_>>` inside `Text`. Also
avoids an allocation when rendering the highlight symbol if it contains
an owned value.
2024-07-26 21:48:29 +03:00
Dheepak Krishnamurthy
efef0d0dc0 chore(ci): change label from breaking change to Type: Breaking Change (#1243)
This PR changes the label that is auto attached to a PR with a breaking
change per the conventional commits specification.
2024-07-26 21:48:05 +03:00
Tayfun Bocek
663486f1e8 perf(list): avoid extra allocations when rendering List (#1244)
When rendering a `List`, each `ListItem` would be cloned. Removing the
clone, and replacing `Widget::render` with `WidgetRef::render_ref` saves
us allocations caused by the clone of the `Text<'_>` stored inside of
`ListItem`.

Based on the results of running the "list" benchmark locally;
Performance is improved by %1-3 for all `render` benchmarks for `List`.
2024-07-26 21:47:30 +03:00
Alex Saveau
7ddfbc0010 fix: unnecessary allocations when creating Lines (#1237)
Signed-off-by: Alex Saveau <saveau.alexandre@gmail.com>
2024-07-26 01:55:04 -07:00
Josh McKinney
3725262ca3 feat(text): Add Add and AddAssign implementations for Line, Span, and Text (#1236)
This enables:

```rust
let line = Span::raw("Red").red() + Span::raw("blue").blue();
let line = Line::raw("Red").red() + Span::raw("blue").blue();
let line = Line::raw("Red").red() + Line::raw("Blue").blue();
let text = Line::raw("Red").red() + Line::raw("Blue").blue();
let text = Text::raw("Red").red() + Line::raw("Blue").blue();

let mut line = Line::raw("Red").red();
line += Span::raw("Blue").blue();

let mut text = Text::raw("Red").red();
text += Line::raw("Blue").blue();

line.extend(vec![Span::raw("1"), Span::raw("2"), Span::raw("3")]);
```
2024-07-26 01:37:49 -07:00
Josh McKinney
84f334163b fix: clippy lints from rust 1.80.0 (#1238) 2024-07-26 00:55:07 -07:00
airblast
03f3124c1d fix(paragraph): line_width, and line_count include block borders (#1235)
The `line_width`, and `line_count` methods for `Paragraph` would not
take into account the `Block` if one was set. This will now correctly
calculate the values including the `Block`'s width/height.

Fixes: #1233
2024-07-23 13:13:50 -07:00
Josh McKinney
c34fb77818 feat(text)!: remove unnecessary lifetime from ToText trait (#1234)
BREAKING CHANGE: The ToText trait no longer has a lifetime parameter.
This change simplifies the trait and makes it easier implement.
2024-07-22 04:24:30 -07:00
Josh McKinney
6ce447c4f3 docs(block): add docs about style inheritance (#1190)
Fixes: https://github.com/ratatui-org/ratatui/issues/1129
2024-07-19 20:02:08 -07:00
Josh McKinney
272d0591a7 docs(paragraph): update main docs (#1202) 2024-07-18 15:59:48 -07:00
Josh McKinney
e81663bec0 refactor(list): split up list.rs into smaller modules (#1204) 2024-07-18 15:59:35 -07:00
EdJoPaTo
7e1bab049b fix(buffer): dont render control characters (#1226) 2024-07-17 13:22:00 +02:00
EdJoPaTo
379dab9cdb build: cleanup dev dependencies (#1231) 2024-07-16 04:35:54 -07:00
Josh McKinney
5b51018501 feat(chart): add GraphType::Bar (#1205)
![Demo](https://vhs.charm.sh/vhs-50v7I5n7lQF7tHCb1VCmFc.gif)
2024-07-15 20:47:50 -07:00
Josh McKinney
7bab9f0d80 chore: add more CompactString::const_new instead of new (#1230) 2024-07-15 20:29:32 -07:00
dependabot[bot]
6d210b3b6b chore(deps): update compact_str requirement from 0.7.1 to 0.8.0 (#1229)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-07-15 14:02:44 -07:00
Liv Haze
935a7187c2 docs(examples): add missing examples to README (#1225)
Resolves: #1014
2024-07-09 21:13:26 -07:00
Josh McKinney
3bb374df88 feat(terminal): add Terminal::try_draw() method (#1209)
This makes it easier to write fallible rendering methods that can use
the `?` operator

```rust
terminal.try_draw(|frame| {
    some_method_that_can_fail()?;
    another_faillible_method()?;
    Ok(())
})?;
```
2024-07-09 00:46:55 -07:00
Liv Haze
50e5674a20 docs(examples): fix typos in tape files (#1224) 2024-07-08 21:57:04 -07:00
Liv Haze
810da72f26 docs(examples): fix hyperlink example tape (#1222) 2024-07-07 20:50:46 -07:00
Emi
7c0665cb0e docs(layout): fix typo in example (#1217) 2024-07-05 04:56:29 -07:00
Josh McKinney
ccf83e6d76 chore: update labels in issue templates (#1212) 2024-06-29 08:38:01 -07:00
Josh McKinney
60bd7f4814 chore(deps): use instabilty instead of stabilty (#1208)
https://github.com/ratatui-org/instability
2024-06-27 08:44:52 -07:00
leohscl
55e0880d2f docs(block): update block documentation (#1206)
Update block documentation with constructor methods and setter methods
in the main doc comment Added an example for using it to surround
widgets

Fixes: https://github.com/ratatui-org/ratatui/issues/914
2024-06-27 04:13:47 -07:00
Josh McKinney
3e7458fdb8 chore(github): add forums and faqs to the issue template (#1201) 2024-06-25 23:13:43 +03:00
tranzystorekk
32a0b26525 refactor: simplify WordWrapper implementation (#1193) 2024-06-25 12:14:32 -07:00
Robert Soane
36d49e549b feat(table): select first, last, etc to table state (#1198)
Add select_previous, select_next, select_first & select_last to
TableState

Used equivalent API as in ListState
2024-06-25 10:45:59 -07:00
Orhun Parmaksız
0a18dcb329 chore(release): prepare for 0.27.0 (#1196)
🧀

Highlights: https://github.com/ratatui-org/ratatui-website/pull/644
2024-06-24 13:59:16 +03:00
Orhun Parmaksız
7ef2daee06 feat(text): support constructing Line and Text from usize (#1167)
Now you can create `Line` and `Text` from numbers like so:

```rust
let line = Line::from(42);
let text = Text::from(666);
```

(I was doing little testing for my TUI app and saw that this isn't
supported - then I was like WHA and decided to make it happen ™️)
2024-06-24 13:23:33 +03:00
Josh McKinney
46977d8851 feat(list)!: add list navigation methods (first, last, previous, next) (#1159)
Also cleans up the list example significantly (see also
<https://github.com/ratatui-org/ratatui/issues/1157>)
    
Fixes: <https://github.com/ratatui-org/ratatui/pull/1159>
    
BREAKING CHANGE: The `List` widget now clamps the selected index to the
bounds of the list when navigating with `first`, `last`, `previous`, and
`next`, as well as when setting the index directly with `select`.
2024-06-24 11:37:22 +03:00
Orhun Parmaksız
38bb196404 docs(breaking-changes): mention LineGauge::gauge_style (#1194)
see #565
2024-06-24 11:27:22 +03:00
Orhun Parmaksız
1908b06b4a docs(borders): add missing closing code blocks (#1195) 2024-06-24 11:27:14 +03:00
Josh McKinney
3f2f2cd6ab feat(docs): add tracing example (#1192)
Add an example that demonstrates logging to a file for:

<https://forum.ratatui.rs/t/how-do-you-println-debug-your-tui-programs/66>

```shell
cargo run --example tracing
RUST_LOG=trace cargo run --example=tracing
cat tracing.log
```

![Made with VHS](https://vhs.charm.sh/vhs-21jgJCedh2YnFDONw0JW7l.gif)
2024-06-19 17:29:19 -07:00
Josh McKinney
efa965e1e8 fix(line): remove newlines when converting strings to Lines (#1191)
`Line::from("a\nb")` now returns a line with two `Span`s instead of 1
Fixes: https://github.com/ratatui-org/ratatui/issues/1111
2024-06-18 21:35:03 -07:00
Josh McKinney
127d706ee4 fix(table): ensure render offset without selection properly (#1187)
Fixes: <https://github.com/ratatui-org/ratatui/issues/1179>
2024-06-18 03:55:24 -07:00
Josh McKinney
1365620606 feat(borders): Add FULL and EMPTY border sets (#1182)
`border::FULL` uses a full block symbol, while `border::EMPTY` uses an
empty space. This is useful for when you need to allocate space for the
border and apply the border style to a block without actually drawing a
border. This makes it possible to style the entire title area or a block
rather than just the title content.

```rust
use ratatui::{symbols::border, widgets::Block};
let block = Block::bordered().title("Title").border_set(border::FULL);
let block = Block::bordered().title("Title").border_set(border::EMPTY);
```
2024-06-17 17:59:24 -07:00
Josh McKinney
cd64367e24 chore(symbols): add tests for line symbols (#1186) 2024-06-17 15:11:42 -07:00
Josh McKinney
07efde5233 docs(examples): add hyperlink example (#1063) 2024-06-17 15:11:02 -07:00
Josh McKinney
308c1df649 docs(readme): add links to forum (#1188) 2024-06-17 17:53:32 +03:00
thscharler
4bfdc15b80 fix: render of &str and String doesn't respect area.width (#1177) 2024-06-16 00:50:24 +02:00
EdJoPaTo
7d175f85c1 refactor(lint): fix new lint warnings (#1178) 2024-06-14 11:26:57 +02:00
Josh McKinney
d370aa75af fix(span): ensure that zero-width characters are rendered correctly (#1165) 2024-06-10 11:51:54 -07:00
Josh McKinney
7fdccafd52 docs(examples): add vhs tapes for constraint-explorer and minimal examples (#1164) 2024-06-08 19:28:07 +03:00
Josh McKinney
10d778866e feat(style): add conversions from the palette crate colors (#1172)
This is behind the "palette" feature flag.

```rust
use palette::{LinSrgb, Srgb};
use ratatui::style::Color;

let color = Color::from(Srgb::new(1.0f32, 0.0, 0.0));
let color = Color::from(LinSrgb::new(1.0f32, 0.0, 0.0));
```
2024-06-07 22:31:46 -07:00
Josh McKinney
e6871b9e21 fix: avoid unicode-width breaking change in tests (#1171)
unicode-width 0.1.13 changed the width of \u{1} from 0 to 1.
Our tests assumed that \u{1} had a width of 0, so this change replaces
the \u{1} character with \u{200B} (zero width space) in the tests.

Upstream issue (closed as won't fix):
https://github.com/unicode-rs/unicode-width/issues/55
2024-06-06 23:06:14 -07:00
Josh McKinney
4f307e69db docs(examples): simplify paragraph example (#1169)
Related: https://github.com/ratatui-org/ratatui/issues/1157
2024-06-05 17:41:46 -07:00
Josh McKinney
7f3efb02e6 fix: pin unicode-width crate to 0.1.13 (#1170)
semver breaking change in 0.1.13
<https://github.com/unicode-rs/unicode-width/issues/55>

<!-- Please read CONTRIBUTING.md before submitting any pull request. -->
2024-06-05 17:26:45 -07:00
Josh McKinney
7b45f74b71 chore(prelude)!: add / remove items (#1149)
his PR removes the items from the prelude that don't form a coherent
common vocabulary and adds the missing items that do.

Based on a comment at
<https://www.reddit.com/r/rust/comments/1cle18j/comment/l2uuuh7/>

BREAKING CHANGE:
The following items have been removed from the prelude:
- `style::Styled` - this trait is useful for widgets that want to
  support the Stylize trait, but it adds complexity as widgets have two
  `style` methods and a `set_style` method.
- `symbols::Marker` - this item is used by code that needs to draw to
  the `Canvas` widget, but it's not a common item that would be used by
  most users of the library.
- `terminal::{CompletedFrame, TerminalOptions, Viewport}` - these items
  are rarely used by code that needs to interact with the terminal, and
  they're generally only ever used once in any app.

The following items have been added to the prelude:
- `layout::{Position, Size}` - these items are used by code that needs
  to interact with the layout system. These are newer items that were
  added in the last few releases, which should be used more liberally.
2024-06-04 20:59:51 -07:00
Josh McKinney
1520ed9d10 feat(layout): impl Display for Position and Size (#1162) 2024-06-03 18:54:50 -07:00
dependabot[bot]
2a74f9d8c1 chore(deps): update rstest requirement from 0.19.0 to 0.21.0 (#1163)
Updates the requirements on [rstest](https://github.com/la10736/rstest)
to permit the latest version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/la10736/rstest/releases">rstest's
releases</a>.</em></p>
<blockquote>
<h2>0.21.0</h2>
<p>Use <code>crate-name</code> feature to enable the crate rename
support (enabled by default)</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/la10736/rstest/blob/master/CHANGELOG.md">rstest's
changelog</a>.</em></p>
<blockquote>
<h2>[0.21.0] 2024/6/1</h2>
<h3>Changed</h3>
<ul>
<li>Add feature <code>crate-name</code> enabled by default to opt-in
crate rename
support. See <a
href="https://redirect.github.com/la10736/rstest/issues/258">#258</a></li>
</ul>
<h2>[0.20.0] 2024/5/30</h2>
<h3>Add</h3>
<ul>
<li>Implemented <code>#[by_ref]</code> attribute to take get a local
lifetime for test arguments.
See <a
href="https://redirect.github.com/la10736/rstest/issues/241">#241</a>
for more details. Thanks to
<a href="https://github.com/narpfel"><code>@​narpfel</code></a> for
suggesting it and useful discussions.</li>
<li>Support for import <code>rstest</code> with another name. See <a
href="https://redirect.github.com/la10736/rstest/issues/221">#221</a></li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Don't remove Lifetimes from test function if any. See <a
href="https://redirect.github.com/la10736/rstest/issues/230">#230</a>
<a href="https://redirect.github.com/la10736/rstest/issues/241">#241</a>
for more details.</li>
<li><a
href="https://doc.rust-lang.org/std/path/struct.PathBuf.html"><code>PathBuf</code></a>
does no longer need to be
in scope when using <code>#[files]</code> (see <a
href="https://redirect.github.com/la10736/rstest/pull/242">#242</a>)</li>
<li><code>#[from(now::accept::also::path::for::fixture)]</code> See <a
href="https://redirect.github.com/la10736/rstest/issues/246">#246</a>
for more details</li>
</ul>
<h2>[0.19.0] 2024/4/9</h2>
<h3>Changed</h3>
<ul>
<li>Defined <code>rust-version</code> for each crate (see <a
href="https://redirect.github.com/la10736/rstest/issues/227">#227</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>
<p><code>#[once]</code> fixtures now require the returned type to be
<a
href="https://doc.rust-lang.org/std/marker/trait.Sync.html"><code>Sync</code></a>
to prevent UB
when tests are executed in parallel. (see <a
href="https://redirect.github.com/la10736/rstest/issues/235">#235</a>
for more details)</p>
</li>
<li>
<p><code>#[future(awt)]</code> and <code>#[awt]</code> now properly
handle mutable (<code>mut</code>) parameters by treating futures as
immutable and
treating the awaited rebinding as mutable.</p>
</li>
</ul>
<h2>[0.18.2] 2023/8/13</h2>
<h3>Changed</h3>
<ul>
<li>Now <code>#[files]</code> accept also parent folders (see <a
href="https://redirect.github.com/la10736/rstest/issues/205">#205</a>
for more details).</li>
</ul>
<h2>[0.18.1] 2023/7/5</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="38da6bfb96"><code>38da6bf</code></a>
Prepare 0.21.0 Release</li>
<li><a
href="ca69788392"><code>ca69788</code></a>
bump version rstest_test to 0.13.0</li>
<li><a
href="b6b43c6740"><code>b6b43c6</code></a>
Clean chackoutlist</li>
<li><a
href="fef4f7b4f4"><code>fef4f7b</code></a>
Implemented Opt-in crate-name support Fix <a
href="https://redirect.github.com/la10736/rstest/issues/258">#258</a></li>
<li><a
href="236be92a8a"><code>236be92</code></a>
Build should use build tests target</li>
<li><a
href="8fde5be94f"><code>8fde5be</code></a>
Prepare next changelog</li>
<li><a
href="f29e6346fe"><code>f29e634</code></a>
Dependency should have a n explicit version to be published</li>
<li><a
href="e27ad2a4db"><code>e27ad2a</code></a>
Removed empty section</li>
<li><a
href="3867794483"><code>3867794</code></a>
Fixed docs</li>
<li><a
href="b90fb8e092"><code>b90fb8e</code></a>
Fix checkout list</li>
<li>Additional commits viewable in <a
href="https://github.com/la10736/rstest/compare/v0.19.0...v0.21.0">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-03 19:38:06 +03:00
dependabot[bot]
d7ed6c8bad chore(deps)!: update termion requirement from 3.0 to 4.0 (#1106)
Updates the requirements on termion to permit the latest version.

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)

</details>

BREAKING CHANGE: Changelog:
<https://gitlab.redox-os.org/redox-os/termion/-/blob/master/CHANGELOG.md>
A change is only necessary if you were matching on all variants of the
`MouseEvent` enum without a
wildcard. In this case, you need to either handle the two new variants,
`MouseLeft` and
`MouseRight`, or add a wildcard.

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-06-02 18:30:46 -07:00
dependabot[bot]
313135c68e chore(deps): update itertools requirement from 0.12 to 0.13 (#1120)
Updates the requirements on
[itertools](https://github.com/rust-itertools/itertools) to permit the
latest version.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rust-itertools/itertools/blob/master/CHANGELOG.md">itertools's
changelog</a>.</em></p>
<blockquote>
<h2>0.13.0</h2>
<h3>Breaking</h3>
<ul>
<li>Removed implementation of <code>DoubleEndedIterator</code> for
<code>ConsTuples</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/853">#853</a>)</li>
<li>Made <code>MultiProduct</code> fused and fixed on an empty iterator
(<a
href="https://redirect.github.com/rust-itertools/itertools/issues/835">#835</a>,
<a
href="https://redirect.github.com/rust-itertools/itertools/issues/834">#834</a>)</li>
<li>Changed <code>iproduct!</code> to return tuples for maxi one
iterator too (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/870">#870</a>)</li>
<li>Changed <code>PutBack::put_back</code> to return the old value (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/880">#880</a>)</li>
<li>Removed deprecated <code>repeat_call, Itertools::{foreach, step,
map_results, fold_results}</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/878">#878</a>)</li>
<li>Removed <code>TakeWhileInclusive::new</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/912">#912</a>)</li>
</ul>
<h3>Added</h3>
<ul>
<li>Added <code>Itertools::{smallest_by, smallest_by_key, largest,
largest_by, largest_by_key}</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/654">#654</a>,
<a
href="https://redirect.github.com/rust-itertools/itertools/issues/885">#885</a>)</li>
<li>Added <code>Itertools::tail</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/899">#899</a>)</li>
<li>Implemented <code>DoubleEndedIterator</code> for
<code>ProcessResults</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/910">#910</a>)</li>
<li>Implemented <code>Debug</code> for <code>FormatWith</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/931">#931</a>)</li>
<li>Added <code>Itertools::get</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/891">#891</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Deprecated <code>Itertools::group_by</code> (renamed
<code>chunk_by</code>) (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/866">#866</a>,
<a
href="https://redirect.github.com/rust-itertools/itertools/issues/879">#879</a>)</li>
<li>Deprecated <code>unfold</code> (use <code>std::iter::from_fn</code>
instead) (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/871">#871</a>)</li>
<li>Optimized <code>GroupingMapBy</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/873">#873</a>,
<a
href="https://redirect.github.com/rust-itertools/itertools/issues/876">#876</a>)</li>
<li>Relaxed <code>Fn</code> bounds to <code>FnMut</code> in
<code>diff_with, Itertools::into_group_map_by</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/886">#886</a>)</li>
<li>Relaxed <code>Debug/Clone</code> bounds for <code>MapInto</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/889">#889</a>)</li>
<li>Documented the <code>use_alloc</code> feature (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/887">#887</a>)</li>
<li>Optimized <code>Itertools::set_from</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/888">#888</a>)</li>
<li>Removed badges in <code>README.md</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/890">#890</a>)</li>
<li>Added &quot;no-std&quot; categories in <code>Cargo.toml</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/894">#894</a>)</li>
<li>Fixed <code>Itertools::k_smallest</code> on short unfused iterators
(<a
href="https://redirect.github.com/rust-itertools/itertools/issues/900">#900</a>)</li>
<li>Deprecated <code>Itertools::tree_fold1</code> (renamed
<code>tree_reduce</code>) (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/895">#895</a>)</li>
<li>Deprecated <code>GroupingMap::fold_first</code> (renamed
<code>reduce</code>) (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/902">#902</a>)</li>
<li>Fixed <code>Itertools::k_smallest(0)</code> to consume the iterator,
optimized <code>Itertools::k_smallest(1)</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/909">#909</a>)</li>
<li>Specialized <code>Combinations::nth</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/914">#914</a>)</li>
<li>Specialized <code>MergeBy::fold</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/920">#920</a>)</li>
<li>Specialized <code>CombinationsWithReplacement::nth</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/923">#923</a>)</li>
<li>Specialized <code>FlattenOk::{fold, rfold}</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/927">#927</a>)</li>
<li>Specialized <code>Powerset::nth</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/924">#924</a>)</li>
<li>Documentation fixes (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/882">#882</a>,
<a
href="https://redirect.github.com/rust-itertools/itertools/issues/936">#936</a>)</li>
<li>Fixed <code>assert_equal</code> for iterators longer than
<code>i32::MAX</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/932">#932</a>)</li>
<li>Updated the <code>must_use</code> message of non-lazy
<code>KMergeBy</code> and <code>TupleCombinations</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/939">#939</a>)</li>
</ul>
<h3>Notable Internal Changes</h3>
<ul>
<li>Tested iterator laziness (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/792">#792</a>)</li>
<li>Created <code>CONTRIBUTING.md</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/767">#767</a>)</li>
</ul>
<h2>0.12.1</h2>
<h3>Added</h3>
<ul>
<li>Documented iteration order guarantee for
<code>Itertools::[tuple_]combinations</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/822">#822</a>)</li>
<li>Documented possible panic in <code>iterate</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/842">#842</a>)</li>
<li>Implemented <code>Clone</code> and <code>Debug</code> for
<code>Diff</code> (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/845">#845</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="d5084d15e9"><code>d5084d1</code></a>
Prepare v0.13.0 release (<a
href="https://redirect.github.com/rust-itertools/itertools/issues/937">#937</a>)</li>
<li><a
href="d7c99d55da"><code>d7c99d5</code></a>
<code>TupleCombinations</code> is not lazy but must be used
nonetheless</li>
<li><a
href="074c7fcc07"><code>074c7fc</code></a>
<code>KMergeBy</code> is not lazy but must be used nonetheless</li>
<li><a
href="2ad9e07ae8"><code>2ad9e07</code></a>
<code>assert_equal</code>: fix
<code>clippy::default_numeric_fallback</code></li>
<li><a
href="0d4efc8432"><code>0d4efc8</code></a>
Remove free function <code>get</code></li>
<li><a
href="05cc0ee256"><code>05cc0ee</code></a>
<code>get(s..=usize::MAX)</code> should be fine when <code>s !=
0</code></li>
<li><a
href="3c16f14baa"><code>3c16f14</code></a>
<code>get</code>: when is it ESI and/or DEI</li>
<li><a
href="4dd6ba0e7c"><code>4dd6ba0</code></a>
<code>get</code>: panics if the range includes
<code>usize::MAX</code></li>
<li><a
href="7a9ce56fc5"><code>7a9ce56</code></a>
<code>get(r: Range)</code> as <code>Skip\&lt;Take&gt;</code></li>
<li><a
href="f676f2f964"><code>f676f2f</code></a>
Remove the unspecified check about
<code>.get(exhausted_range_inclusive)</code></li>
<li>Additional commits viewable in <a
href="https://github.com/rust-itertools/itertools/compare/v0.12.0...v0.13.0">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
Dependabot will merge this PR once CI passes on it, as requested by
@joshka.

[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-06-02 18:18:36 -07:00
Josh McKinney
8061813f32 refactor: expand glob imports (#1152)
Consensus is that explicit imports make it easier to understand the
example code. This commit removes the prelude import from all examples
and replaces it with the necessary imports, and expands other glob
imports (widget::*, Constraint::*, KeyCode::*, etc.) everywhere else.
Prelude glob imports not in examples are not covered by this PR.

See https://github.com/ratatui-org/ratatui/issues/1150 for more details.
2024-05-29 04:42:29 -07:00
Josh McKinney
74a32afbae feat: re-export backends from the ratatui crate (#1151)
`crossterm`, `termion`, and `termwiz` can now be accessed as
`ratatui::{crossterm, termion, termwiz}` respectively. This makes it
possible to just add the Ratatui crate as a dependency and use the
backend of choice without having to add the backend crates as
dependencies.

To update existing code, replace all instances of `crossterm::` with
`ratatui::crossterm::`, `termion::` with `ratatui::termion::`, and
`termwiz::` with `ratatui::termwiz::`.
2024-05-28 13:23:39 -07:00
EdJoPaTo
4ce67fc84e perf(buffer)!: filled moves the cell to be filled (#1148) 2024-05-27 11:07:27 -07:00
EdJoPaTo
df4b706674 style: enable more rustfmt settings (#1125) 2024-05-26 19:50:10 +02:00
EdJoPaTo
8b447ec4d6 perf(rect)!: Rect::inner takes Margin directly instead of reference (#1008)
BREAKING CHANGE: Margin needs to be passed without reference now.

```diff
-let area = area.inner(&Margin {
+let area = area.inner(Margin {
     vertical: 0,
     horizontal: 2,
 });
```
2024-05-26 19:44:46 +02:00
EdJoPaTo
7a48c5b11b feat(cell): add EMPTY and (const) new method (#1143)
This simplifies calls to `Buffer::filled` in tests.
2024-05-25 14:08:56 -07:00
Josh McKinney
8cfc316bcc chore: alphabetize examples in Cargo.toml (#1145) 2024-05-25 14:03:37 -07:00
EdJoPaTo
2f8a9363fc docs: fix links on docs.rs (#1144)
This also results in a more readable Cargo.toml as the locations of the
things are more obvious now.

Includes rewording of the underline-color feature.

Logs of the errors: https://docs.rs/crate/ratatui/0.26.3/builds/1224962
Also see #989
2024-05-25 10:38:33 -07:00
EdJoPaTo
d92997105b refactor: dont manually impl Default for defaults (#1142)
Replace `impl Default` by `#[derive(Default)]` when its implementation
equals.
2024-05-25 10:34:48 -07:00
EdJoPaTo
42cda6d287 fix: prevent panic from string_slice (#1140)
<https://rust-lang.github.io/rust-clippy/master/index.html#string_slice>
2024-05-25 15:15:08 +02:00
EdJoPaTo
4f7791079e refactor(padding): Add Padding::ZERO as a constant (#1133)
Deprecate Padding::zero()
2024-05-24 23:48:05 -07:00
EdJoPaTo
cf67ed9b88 refactor(lint): use clippy::or_fun_call (#1138)
<https://rust-lang.github.io/rust-clippy/master/index.html#or_fun_call>
2024-05-24 18:33:19 -07:00
EdJoPaTo
8a60a561c9 refactor: needless_pass_by_ref_mut (#1137)
<https://rust-lang.github.io/rust-clippy/master/index.html#needless_pass_by_ref_mut>
2024-05-24 18:32:22 -07:00
Dheepak Krishnamurthy
35941809e1 feat!: Make Stylize's .bg(color) generic (#1103) 2024-05-24 18:05:45 -07:00
EdJoPaTo
73fd367a74 refactor(block): group builder pattern methods (#1134) 2024-05-24 18:04:35 -07:00
EdJoPaTo
1de9a82b7a refactor: simplify if let (#1135)
While looking through lints
[`clippy::option_if_let_else`](https://rust-lang.github.io/rust-clippy/master/index.html#option_if_let_else)
found these. Other findings are more complex so I skipped them.
2024-05-24 18:03:51 -07:00
EdJoPaTo
d6587bc6b0 test(style): use rstest (#1136)
<!-- Please read CONTRIBUTING.md before submitting any pull request. -->
2024-05-24 18:02:59 -07:00
Matt Armstrong
f429f688da docs(examples): Remove lifetimes from the List example (#1132)
Simplify the List example by removing lifetimes not strictly necessary
to demonstrate how Ratatui lists work. Instead, the sample strings are
copied into each `TodoItem`. To further simplify, I changed the code to
use a new TodoItem::new function, rather than an implementation of the
`From` trait.
2024-05-24 15:09:24 -07:00
Valentin271
4770e71581 refactor(list)!: remove deprecated start_corner and Corner (#759)
`List::start_corner` was deprecated in v0.25. Use `List::direction` and
`ListDirection` instead.

```diff
- list.start_corner(Corner::TopLeft);
- list.start_corner(Corner::TopRight);
// This is not an error, BottomRight rendered top to bottom previously
- list.start_corner(Corner::BottomRight);
// all becomes
+ list.direction(ListDirection::TopToBottom);
```

```diff
- list.start_corner(Corner::BottomLeft);
// becomes
+ list.direction(ListDirection::BottomToTop);
```

`layout::Corner` is removed entirely.

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-05-24 11:52:01 -07:00
Mikołaj Nowak
eef1afe915 feat(LineGauge): allow LineGauge background styles (#565)
This PR deprecates `gauge_style` in favor of `filled_style` and
`unfilled_style` which can have it's foreground and background styled.

`cargo run --example=line_gauge --features=crossterm`

https://github.com/ratatui-org/ratatui/assets/5149215/5fb2ce65-8607-478f-8be4-092e08612f5b

Implements: <https://github.com/ratatui-org/ratatui/issues/424>

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-05-24 11:42:52 -07:00
EdJoPaTo
257db6257f refactor(cell): must_use and simplify style() (#1124)
<!-- Please read CONTRIBUTING.md before submitting any pull request. -->
2024-05-21 23:26:32 -07:00
EdJoPaTo
70df102de0 build(bench): improve benchmark consistency (#1126)
Codegen units are optimized on their own. Per default bench / release
have 16 codegen units. What ends up in a codeget unit is rather random
and can influence a benchmark result as a code change can move stuff
into a different codegen unit → prevent / allow LLVM optimizations
unrelated to the actual change.

More details: https://doc.rust-lang.org/cargo/reference/profiles.html
2024-05-21 23:12:31 -07:00
EdJoPaTo
bf2036987f refactor(cell): reset instead of applying default (#1127)
Using reset is clearer to me what actually happens. On the other case a
struct is created to override the old one completely which basically
does the same in a less clear way.
2024-05-21 22:11:45 -07:00
Enrico Borba
0b5fd6bf8e feat: add writer() and writer_mut() to termion and crossterm backends (#991)
It is sometimes useful to obtain access to the writer if we want to see
what has been written so far. For example, when using &mut [u8] as a
writer.

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-05-21 10:34:07 -07:00
Orhun Parmaksız
fadc73d62e chore(release): prepare for 0.26.3 (#1118)
🧀

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-05-20 15:20:29 +03:00
Josh McKinney
fcb5d589bb fix: make cargo test --doc work with unstable-widget-ref examples (#1117) 2024-05-19 16:18:45 -07:00
Josh McKinney
4955380932 build: remove pre-push hooks (#1115) 2024-05-18 22:09:15 -07:00
Josh McKinney
828d17a3f5 docs: add minimal example (#1114) 2024-05-18 21:18:59 -07:00
EdJoPaTo
9bd89c218a refactor(clippy): enable breaking lint checks (#988)
We need to make sure to not change existing methods without a notice.
But at the same time this also finds public additions with mistakes
before they are even released which is what I would like to have.

This renames a method and deprecated the old name hinting to a new name.
Should this be mentioned somewhere, so it's added to the release notes?
It's not breaking because the old method is still there.
2024-05-13 18:16:09 -07:00
EdJoPaTo
2cfe82a47e refactor(buffer): deprecate assert_buffer_eq! in favor of assert_eq! (#1007)
- Simplify `assert_buffer_eq!` logic.
- Deprecate `assert_buffer_eq!`.
- Introduce `TestBackend::assert_buffer_lines`.

Also simplify many tests involving buffer comparisons.

For the deprecation, just use `assert_eq` instead of `assert_buffer_eq`:

```diff
-assert_buffer_eq!(actual, expected);
+assert_eq!(actual, expected);
```

---

I noticed `assert_buffer_eq!` creating no test coverage reports and
looked into this macro. First I simplified it. Then I noticed a bunch of
`assert_eq!(buffer, …)` and other indirect usages of this macro (like
`TestBackend::assert_buffer`).

The good thing here is that it's mainly used in tests so not many
changes to the library code.
2024-05-13 18:13:46 -07:00
tranzystorekk
1a4bb1cbb8 perf(layout): avoid allocating memory when using split ergonomic utils (#1105)
Don't create intermediate vec in `Layout::areas` and
`Layout::spacers` when there's no need for one.
2024-05-13 16:54:34 -07:00
Dheepak Krishnamurthy
839cca20bf docs(table): Fix typo in docs for highlight_symbol (#1108) 2024-05-13 16:38:34 -04:00
Orhun Parmaksız
f945a0bcff docs(test): fix typo in TestBackend documentation (#1107) 2024-05-13 23:15:09 +03:00
EdJoPaTo
eb281df974 feat: use inner Display implementation (#1097) 2024-05-13 01:36:39 -07:00
Josh McKinney
28e81c0714 build: add underline-color to all features flag in makefile (#1100) 2024-05-13 01:34:52 -07:00
Dheepak Krishnamurthy
76e5fe5a9a chore: Revert "Make Stylize's .bg(color) generic" (#1102)
This reverts commit ec763af851 from #1099
2024-05-12 19:05:00 -04:00
EdJoPaTo
366cbae09f fix(buffer): fix Debug panic and fix formatting of overridden parts (#1098)
Fix panic in `Debug for Buffer` when `width == 0`.
Also corrects the output when symbols are overridden.
2024-05-12 14:57:40 -07:00
Dheepak Krishnamurthy
ec763af851 feat: Make Stylize's .bg(color) generic (#1099)
This PR makes `.bg(color)` generic accepting anything that can be
converted into `Color`; similar to the `.fg(color)` method on the same
trait
2024-05-12 13:23:08 -04:00
Josh McKinney
699c2d7c8d fix: unicode truncation bug (#1089)
- Rewrote the line / span rendering code to take into account how
multi-byte / wide emoji characters are truncated when rendering into
areas that cannot accommodate them in the available space
- Added comprehensive coverage over the edge cases
- Adds a benchmark to ensure perf

Fixes: https://github.com/ratatui-org/ratatui/issues/1032
Co-authored-by: EdJoPaTo <rfc-conform-git-commit-email@funny-long-domain-label-everyone-hates-as-it-is-too-long.edjopato.de>
Co-authored-by: EdJoPaTo <github@edjopato.de>
2024-05-11 19:28:38 -07:00
EdJoPaTo
3cc29bdada test(block): use rstest to simplify test cases (#1095) 2024-05-11 11:03:02 -07:00
Josh McKinney
8de3d52469 chore(changelog): put commit id on the same line as the message (#1093) 2024-05-11 15:30:08 +03:00
Josh McKinney
b30411d1c7 fix: termwiz underline color test (#1094)
Fixes code that doesn't compile in the termwiz tests when
underline-color feature is enabled.
2024-05-10 20:04:58 -07:00
Josh McKinney
aa4260f92c style: use std::fmt instead of importing Debug and Display (#1087)
This is a small universal style change to avoid making this change a
part of other PRs.

[rationale](https://github.com/ratatui-org/ratatui/pull/1083#discussion_r1588466060)
2024-05-04 23:51:24 -07:00
Josh McKinney
5f1e119563 fix: correct feature flag typo for termwiz (#1088)
underline-color was incorrectly spelt as underline_color
2024-05-04 23:43:24 -07:00
Josh McKinney
4d1784f2de feat: re-export ParseColorError as style::ParseColorError (#1086)
Fixes: https://github.com/ratatui-org/ratatui/issues/1085
2024-05-04 14:22:51 -07:00
EdJoPaTo
baedc39494 refactor(buffer): simplify set_stringn logic (#1083) 2024-05-02 16:32:52 -07:00
EdJoPaTo
366c2a0e6d perf(block): use Block::bordered (#1041)
`Block::bordered()` is shorter than
`Block::new().borders(Borders::ALL)`, requires one less import
(`Borders`) and in case `Block::default()` was used before can even be
`const`.
2024-05-02 03:09:48 -07:00
Josh McKinney
bef2bc1e7c chore(cargo): add homepage to Cargo.toml (#1080) 2024-05-02 12:47:10 +03:00
Josh McKinney
64eb3913a4 chore: fixup cargo lint for windows targets (#1071)
Crossterm brings in multiple versions of the same dep
2024-05-01 05:59:09 -07:00
May
e95230beda docs: add note about scrollbar state content length (#1077) 2024-04-30 23:37:43 -07:00
Levi Zim
f4637d40c3 fix(reflow): allow wrapping at zero width whitespace (#1074) 2024-04-28 01:58:28 -07:00
Orhun Parmaksız
da1ade7b2e docs(github): update code owners about past maintainers (#1073)
As per suggestion in
https://github.com/ratatui-org/ratatui/pull/1067#issuecomment-2079766990

It's good for historical purposes!
2024-04-27 16:06:29 -07:00
Josh McKinney
1706b0a3e4 perf(crossterm): Speed up combined fg and bg color changes by up to 20% (#1072) 2024-04-27 14:34:52 -07:00
Eiko Thomas
4392759501 fix(examples): changed user_input example to work with multi-byte unicode chars (#1069)
This is the proposed solution for issue #1068. It solves the bug in the
user_input example with multi-byte UTF-8 characters as input.

Fixes: #1068

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-04-26 22:36:36 -07:00
Paul Sobolik
20fc0ddfca fix(examples): fix key handling in constraints (#1066)
Add check for `KeyEventKind::Press` to constraints example's event
handler to eliminate double keys
on Windows.

Fixes: #1062

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-04-26 21:20:30 -07:00
Orhun Parmaksız
3687f78f6a docs(github): update code owners (#1067)
Removes the team members that are not able to review PRs recently (with
their approval ofc)
2024-04-26 20:51:06 -07:00
Tadeas Uradnik
5fbb77ad20 chore(README): use terminal theme for badges (#1026)
The badges in the readme were all the default theme. Giving them
prettier colors that match the terminal gif is better. I've used the
colors from the VHS repo.
2024-04-25 20:27:28 -07:00
EdJoPaTo
97ee102f17 feat(buffer): track_caller for index_of (#1046)
The caller put in the wrong x/y -> the caller is the cause.
2024-04-25 16:23:09 -07:00
EdJoPaTo
c442dfd1ad perf(canvas): change map data to const instead of static (#1037) 2024-04-24 01:49:57 -07:00
Ibby
0a164965ea fix: use to_string to serialize Color (#934)
Since deserialize now uses `FromStr` to deserialize color, serializing
`Color` RGB values, as well as index values, would produce an output
that would no longer be able to be deserialized without causing an
error.


Color::Rgb will now be serialized as the hex representation of their
value.
For example, with serde_json, `Color::Rgb(255, 0, 255)` would be
serialized as `"#FF00FF"` rather than `{"Rgb": [255, 0, 255]}`.

Color::Indexed will now be serialized as just the string of the index.
For example, with serde_json, `Color::Indexed(10)` would be serialized
as `"10"` rather than `{"Indexed": 10}`.

Other color variants remain the same.
2024-04-23 23:30:00 -07:00
EdJoPaTo
bf0923473c feat(table): make TableState::new const (#1040) 2024-04-21 23:23:56 -06:00
EdJoPaTo
81b96338ea perf(calendar): use const fn (#1039)
Also, do the comparison without `as u8`. Stays the same at runtime and
is cleaner code.
2024-04-21 11:34:17 -06:00
EdJoPaTo
2e71c1874e perf(buffer): simplify Buffer::filled with macro (#1036)
The `vec![]` macro is highly optimized by the Rust team and shorter.
Don't do it manually.

This change is mainly cleaner code. The only production code that uses
this is `Terminal::with_options` and `Terminal::insert_before` so it's
not performance relevant on every render.
2024-04-21 11:29:37 -06:00
Josh McKinney
c75aa1990f build: add clippy::cargo lint (#1053)
Followup to https://github.com/ratatui-org/ratatui/pull/1035 and
https://github.com/ratatui-org/ratatui/discussions/1034

It's reasonable to enable this and deal with breakage by fixing any
specific issues that arise.
2024-04-21 10:43:46 -06:00
mcskware
326a461f9a chore: Add package categories field (#1035)
Add the package categories field in Cargo.toml, with value
`["command-line-interface"]`. This fixes the (currently non-default)
clippy cargo group lint
[`clippy::cargo_common_metadata`](https://rust-lang.github.io/rust-clippy/master/index.html#/cargo_common_metadata).

As per discussion in [Cargo package categories
suggestions](https://github.com/ratatui-org/ratatui/discussions/1034),
this lint is not suggested to be run by default in CI, but rather as an
occasional one-off as part of the larger
[`clippy::cargo`](https://doc.rust-lang.org/stable/clippy/lints.html#cargo)
lint group.
2024-04-19 12:55:12 -07:00
EdJoPaTo
f3172c59d4 refactor(gauge): fix internal typo (#1048) 2024-04-18 13:47:52 +03:00
EdJoPaTo
bef5bcf750 refactor(example): remove pointless new method (#1038)
Use `App::default()` directly.
2024-04-16 21:02:39 +03:00
Marcin Puc
11264787d0 chore(changelog): fix changelog release links (#1033)
<!-- Please read CONTRIBUTING.md before submitting any pull request. -->

Minor oopsie
2024-04-15 23:11:27 +03:00
dependabot[bot]
9b3c260b76 chore(deps): update rstest requirement from 0.18.2 to 0.19.0 (#1031)
Updates the requirements on [rstest](https://github.com/la10736/rstest)
to permit the latest version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/la10736/rstest/releases">rstest's
releases</a>.</em></p>
<blockquote>
<p>Introduce MSRV and minor fixes</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/la10736/rstest/blob/master/CHANGELOG.md">rstest's
changelog</a>.</em></p>
<blockquote>
<h2>[0.19.0] 2024/4/9</h2>
<h3>Changed</h3>
<ul>
<li>Defined <code>rust-version</code> for each crate (see <a
href="https://redirect.github.com/la10736/rstest/issues/227">#227</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>
<p><code>#[once]</code> fixtures now require the returned type to be
<a
href="https://doc.rust-lang.org/std/marker/trait.Sync.html"><code>Sync</code></a>
to prevent UB
when tests are executed in parallel. (see <a
href="https://redirect.github.com/la10736/rstest/issues/235">#235</a>
for more details)</p>
</li>
<li>
<p><code>#[future(awt)]</code> and <code>#[awt]</code> now properly
handle mutable (<code>mut</code>) parameters by treating futures as
immutable and
treating the awaited rebinding as mutable.</p>
</li>
</ul>
<h2>[0.18.2] 2023/8/13</h2>
<h3>Changed</h3>
<ul>
<li>Now <code>#[files]</code> accept also parent folders (see <a
href="https://redirect.github.com/la10736/rstest/issues/205">#205</a>
for more details).</li>
</ul>
<h2>[0.18.1] 2023/7/5</h2>
<h3>Fixed</h3>
<ul>
<li>Wrong doc test</li>
<li>Docs</li>
</ul>
<h2>[0.18.0] 2023/7/4</h2>
<h3>Add</h3>
<ul>
<li>Add support for <code>RSTEST_TIMEOUT</code> environment variable to
define a max timeout
for each function (see <a
href="https://redirect.github.com/la10736/rstest/issues/190">#190</a>
for details).
Thanks to <a
href="https://github.com/aviramha"><code>@​aviramha</code></a> for idea
and PR</li>
<li><code>#[files(&quot;glob path&quot;)]</code> attribute to generate
tests based on files that
satisfy the given glob path (see <a
href="https://redirect.github.com/la10736/rstest/issues/163">#163</a>
for details).</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Switch to <code>syn</code> 2.0 and edition 2021 : minimal Rust
version now is 1.56.0
both for <code>rstest</code> and <code>rstest_reuse</code> (see <a
href="https://redirect.github.com/la10736/rstest/issues/187">#187</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Fixed wired behavior on extraction <code>#[awt]</code> function
attrs (See
<a
href="https://redirect.github.com/la10736/rstest/issues/189">#189</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="3ffd682568"><code>3ffd682</code></a>
Fix license links</li>
<li><a
href="36ab06d68e"><code>36ab06d</code></a>
Fix license link</li>
<li><a
href="941d8ac7ae"><code>941d8ac</code></a>
Update changelog</li>
<li><a
href="cdff674d16"><code>cdff674</code></a>
Bump version</li>
<li><a
href="e0624fe198"><code>e0624fe</code></a>
Fix clippy warning</li>
<li><a
href="f7b4b57922"><code>f7b4b57</code></a>
Shutup warning on nightly (tests)</li>
<li><a
href="49a7d3816e"><code>49a7d38</code></a>
Shutup warning in night</li>
<li><a
href="b58ce22ef1"><code>b58ce22</code></a>
Set resolver in virtual manifest</li>
<li><a
href="3c2fb9c33c"><code>3c2fb9c</code></a>
Properly handle mutability for awaited futures (<a
href="https://redirect.github.com/la10736/rstest/issues/239">#239</a>)</li>
<li><a
href="61a7007f66"><code>61a7007</code></a>
We're not interested about msrv for tests</li>
<li>Additional commits viewable in <a
href="https://github.com/la10736/rstest/compare/v0.18.2...v0.19.0">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-15 18:55:45 +03:00
Orhun Parmaksız
363c4c54e8 chore(release): prepare for 0.26.2 (#1029) 2024-04-15 13:38:55 +03:00
Josh McKinney
b7778e5cd1 fix(paragraph): unit test typo (#1022) 2024-04-08 17:33:05 -07:00
dependabot[bot]
b5061c5250 chore(deps): update stability requirement from 0.1.1 to 0.2.0 (#1021)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-04-08 22:43:04 +03:00
EdJoPaTo
359204c929 refactor: simplify to io::Result (#1016)
Simplifies the code, logic stays exactly the same.
2024-04-03 02:08:42 -07:00
EdJoPaTo
14461c3a35 docs(breaking-changes): typos and markdownlint (#1009) 2024-04-03 02:07:39 -07:00
Josh McKinney
3b002fdcab docs: update incompatible code warning in examples readme (#1013)
Co-authored-by: EdJoPaTo <rfc-conform-git-commit-email@funny-long-domain-label-everyone-hates-as-it-is-too-long.edjopato.de>
2024-04-01 13:34:03 -07:00
Tadeas Uradnik
0207160784 fix(line): line truncation respects alignment (#987)
When rendering a `Line`, the line will be truncated:
- on the right for left aligned lines
- on the left for right aligned lines
- on bot sides for centered lines

E.g. "Hello World" will be rendered as "Hello", "World", "lo wo" for
left, right, centered lines respectively.

Fixes: https://github.com/ratatui-org/ratatui/issues/932
2024-03-30 18:10:33 -07:00
Josh McKinney
26af65043e feat(text): add push methods for text and line (#998)
Adds the following methods to the `Text` and `Line` structs:
- Text::push_line
- Text::push_span
- Line::push_span

This allows for adding lines and spans to a text object without having
to call methods on the fields directly, which is usefult for incremental
construction of text objects.
2024-03-28 15:30:21 -07:00
Benjamin Nickolls
07da90a718 chore(funding): add eth address for receiving funds from drips.network (#994) 2024-03-27 18:11:26 +03:00
Orhun Parmaksız
125ee929ee chore(docs): fix typos in crate documentation (#1002) 2024-03-27 16:28:23 +03:00
Josh McKinney
742a5ead06 fix(text): fix panic when rendering out of bounds (#997)
Previously it was possible to cause a panic when rendering to an area
outside of the buffer bounds. Instead this now correctly renders nothing
to the buffer.
2024-03-24 22:06:31 -07:00
EdJoPaTo
8719608bda refactor(span): rename to_aligned_line into into_aligned_line (#993)
With the Rust method naming conventions these methods are into methods
consuming the Span. Therefore, it's more consistent to use `into_`
instead of `to_`.

```rust
Span::to_centered_line
Span::to_left_aligned_line
Span::to_right_aligned_line
```

Are marked deprecated and replaced with the following

```rust
Span::into_centered_line
Span::into_left_aligned_line
Span::into_right_aligned_line
```
2024-03-22 04:29:29 -07:00
Josh McKinney
f6c4e447e6 fix: ensure that paragraph correctly renders styled text (#992)
Paragraph was ignoring the new `Text::style` field added in 0.26.0

Fixes: https://github.com/ratatui-org/ratatui/issues/990
2024-03-22 02:14:59 -07:00
Jack Wills
c56f49b9fb fix(list): saturating_sub to fix highlight_symbol overflow (#949)
An overflow (pedantically an underflow) can occur if the
highlight_symbol is a multi-byte char, and area is reduced to a size
less than that char length.
2024-03-16 15:58:39 -07:00
Orhun Parmaksız
078e97e4ff chore(github): add EdJoPaTo as a maintainer (#986) 2024-03-16 19:29:57 +01:00
EdJoPaTo
8e68db9e2f refactor: remove pointless default on internal structs (#980)
See #978

Also remove other derives. They are unused and just slow down
compilation.
2024-03-12 00:33:30 -07:00
EdJoPaTo
541f0f9953 perf(cell): use const CompactString::new_inline (#979)
Some minor find when messing around trying to `const` all the things.

While `reset()` and `default()` can not be `const` it's still a benefit
when their contents are.
2024-03-05 22:05:24 +01:00
Josh McKinney
88bfb5a430 docs(text): update Text and Line docs (#969) 2024-03-05 22:02:07 +01:00
EdJoPaTo
c4ce7e8ff6 build: enable more satisfied lints (#974)
These lints dont generate warnings and therefore dont need refactoring.
I think they are useful in the future.
2024-03-03 21:41:24 -08:00
EdJoPaTo
3be189e3c6 refactor: clippy::thread_local_initializer_can_be_made_const (#974)
enabled by default on nightly
2024-03-03 21:41:24 -08:00
EdJoPaTo
5c4efacd1d refactor: clippy::map_err_ignore (#974) 2024-03-03 21:41:24 -08:00
EdJoPaTo
bbb6d65e06 refactor: clippy::else_if_without_else (#974) 2024-03-03 21:41:24 -08:00
EdJoPaTo
fdb14dc7cd refactor: clippy::redundant_type_annotations (#974) 2024-03-03 21:41:23 -08:00
EdJoPaTo
9b3b23ac14 refactor: remove literal suffix (#974)
its not needed and can just be assumed

related: clippy::(un)separated_literal_suffix
2024-03-03 21:41:23 -08:00
EdJoPaTo
58b6e0be0f refactor: clippy::should_panic_without_expect (#974) 2024-03-03 21:41:23 -08:00
EdJoPaTo
6e6ba27a12 build(lint): warn on pedantic and allow the rest (#974) 2024-03-03 21:41:23 -08:00
EdJoPaTo
c870a41057 refactor: clippy::many_single_char_names (#974) 2024-03-03 21:41:23 -08:00
EdJoPaTo
a6036ad789 refactor: clippy::similar_names (#974) 2024-03-03 21:41:23 -08:00
EdJoPaTo
060d26b6dc refactor: clippy::match_same_arms (#974) 2024-03-03 21:41:23 -08:00
EdJoPaTo
a4e84a6a7f build!: increase msrv to 1.74.0 (#974)
configure lints in Cargo.toml requires 1.74.0

BREAKING CHANGE: rust 1.74 is required now
2024-03-03 21:41:22 -08:00
EdJoPaTo
fcbea9ee68 refactor: clippy::uninlined_format_args (#974) 2024-03-03 21:41:22 -08:00
EdJoPaTo
14b24e7585 refactor: clippy::if_not_else (#974) 2024-03-03 21:41:22 -08:00
EdJoPaTo
5ed1f43c62 refactor: clippy::redundant_closure_for_method_calls (#974) 2024-03-03 21:41:22 -08:00
EdJoPaTo
c8c7924e0c refactor: clippy::too_many_lines (#974) 2024-03-03 21:41:22 -08:00
EdJoPaTo
e3afe7c8a1 refactor: clippy::unreadable_literal (#974) 2024-03-03 21:41:22 -08:00
EdJoPaTo
a1f54de7d6 refactor: clippy::bool_to_int_with_if (#974) 2024-03-03 21:41:22 -08:00
EdJoPaTo
b8ea190bf2 refactor: clippy::cast_lossless (#974) 2024-03-03 21:41:21 -08:00
EdJoPaTo
0de5238ed3 refactor: dead_code (#974)
enabled by default, only detected by nightly yet
2024-03-03 21:41:21 -08:00
EdJoPaTo
df5dddfbc9 refactor: unused_imports (#974)
enabled by default, only detected on nightly yet
2024-03-03 21:41:21 -08:00
EdJoPaTo
f1398ae6cb refactor: clippy::useless_vec (#974)
Lint enabled by default but only nightly finds this yet
2024-03-03 21:41:21 -08:00
EdJoPaTo
525848ff4e refactor: manually apply clippy::use_self for impl with lifetimes (#974) 2024-03-03 21:41:21 -08:00
EdJoPaTo
660c7183c7 refactor: clippy::empty_line_after_doc_comments (#974) 2024-03-03 21:41:21 -08:00
EdJoPaTo
ab951fae81 refactor: clippy::return_self_not_must_use (#974) 2024-03-03 21:41:21 -08:00
EdJoPaTo
3cd4369176 refactor: clippy::doc_markdown (#974) 2024-03-03 21:41:20 -08:00
EdJoPaTo
9bc014d7f1 refactor: clippy::items_after_statements (#974) 2024-03-03 21:41:20 -08:00
EdJoPaTo
36a0cd56e5 refactor: clippy::deref_by_slicing (#974) 2024-03-03 21:41:20 -08:00
EdJoPaTo
b831c5688c refactor(widget-ref): clippy::needless_pass_by_value (#974) 2024-03-03 21:41:20 -08:00
EdJoPaTo
8195f526cb perf: clippy::needless_pass_by_value (#974) 2024-03-03 21:41:20 -08:00
EdJoPaTo
f7f66928a8 refactor: clippy::equatable_if_let (#974) 2024-03-03 21:41:20 -08:00
EdJoPaTo
01418eb7c2 refactor: clippy::default_trait_access (#974) 2024-03-03 21:41:20 -08:00
EdJoPaTo
8536760e78 refactor: clippy::inefficient_to_string (#974) 2024-03-03 21:41:20 -08:00
EdJoPaTo
a558b19c9a refactor: clippy::implicit_clone (#974) 2024-03-03 21:41:19 -08:00
EdJoPaTo
183c07ef43 perf: clippy::trivially_copy_pass_by_ref (#974) 2024-03-03 21:41:19 -08:00
EdJoPaTo
a13867ffce perf: clippy::cloned_instead_of_copied (#974) 2024-03-03 21:41:19 -08:00
EdJoPaTo
5b00e3aae9 refactor: clippy::use_self (#974) 2024-03-03 21:41:19 -08:00
EdJoPaTo
38c17e091c chore(editorconfig): set and apply some defaults (#974) 2024-03-03 21:41:19 -08:00
EdJoPaTo
3834374652 perf: clippy::missing_const_for_fn (#974) 2024-03-03 21:41:19 -08:00
EdJoPaTo
27680c05ce refactor: clippy::semicolon_if_nothing_returned (#974) 2024-03-03 21:41:19 -08:00
EdJoPaTo
6fd5f631bb refactor(lint): prefer idiomatic for loops (#974) 2024-03-03 21:41:18 -08:00
EdJoPaTo
37b957c7e1 refactor(lints): add lints to scrollbar (#974) 2024-03-03 21:41:18 -08:00
EdJoPaTo
e02f4768ce perf(borders): allow border!() in const (#977)
This allows more compiler optimizations when the macro is used.
2024-03-03 21:17:28 -08:00
EdJoPaTo
c12bcfefa2 refactor(non-src): apply pedantic lints (#976)
Fixes many not yet enabled lints (mostly pedantic) on everything that is
not the lib (examples, benchs, tests). Therefore, this is not containing
anything that can be a breaking change.

Lints are not enabled as that should be the job of #974. I created this
as a separate PR as its mostly independent and would only clutter up the
diff of #974 even more.

Also see
https://github.com/ratatui-org/ratatui/pull/974#discussion_r1506458743

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-03-02 01:06:53 -08:00
dependabot[bot]
94f4547dcf chore(deps): bump orhun/git-cliff-action from 2 to 3 (#972) 2024-02-26 20:19:34 +01:00
dependabot[bot]
3a6b8808ed chore(deps): update derive_builder requirement from 0.13.0 to 0.20.0 (#960) 2024-02-26 01:23:20 -08:00
Josh McKinney
1cff511934 feat(line): impl Styled for Line (#968)
This adds `FromIterator` impls for `Line` and `Text` that allow creating
`Line` and `Text` instances from iterators of `Span` and `Line`
instances, respectively.

```rust
let line = Line::from_iter(vec!["Hello".blue(), " world!".green()]);
let line: Line = iter::once("Hello".blue())
    .chain(iter::once(" world!".green()))
    .collect();
let text = Text::from_iter(vec!["The first line", "The second line"]);
let text: Text = iter::once("The first line")
    .chain(iter::once("The second line"))
    .collect();
```
2024-02-25 05:14:19 -08:00
Josh McKinney
b5bdde079e feat(text): add FromIterator impls for Line and Text (#967)
This adds `FromIterator` impls for `Line` and `Text` that allow creating
`Line` and `Text` instances from iterators of `Span` and `Line`
instances, respectively.

```rust
let line = Line::from_iter(vec!["Hello".blue(), " world!".green()]);
let line: Line = iter::once("Hello".blue())
    .chain(iter::once(" world!".green()))
    .collect();
let text = Text::from_iter(vec!["The first line", "The second line"]);
let text: Text = iter::once("The first line")
    .chain(iter::once("The second line"))
    .collect();
```
2024-02-25 05:13:32 -08:00
Cameron Barnes
654949bb00 feat(list): Add Scroll Padding to Lists (#958)
Introduces scroll padding, which allows the api user to request that a certain number of ListItems be kept visible above and below the currently selected item while scrolling.

```rust
let list = List::new(items).scroll_padding(1);
```

Fixes: https://github.com/ratatui-org/ratatui/pull/955
2024-02-24 19:11:29 -08:00
EdJoPaTo
943c0431d9 fix(scrollbar): dont render on 0 length track (#964)
Fixes a panic when `track_length - 1` is used. (clamp panics on `-1.0`
being smaller than `0.0`)
2024-02-24 19:21:24 +01:00
EdJoPaTo
65e7923753 perf(scrollbar): const creation (#963)
A bunch of `const fn` allow for more performance and `Default` now uses the `const` new implementations.
2024-02-24 18:29:42 +01:00
Orhun Parmaksız
d0067c8815 docs(license): update copyright years (#962) 2024-02-21 11:24:38 +01:00
ThomasMiz
35e971f7eb fix: scrollbar thumb not visible on long lists (#959)
When displaying somewhat-long lists, the `Scrollbar` widget sometimes did not display a thumb character, and only the track will be visible.
2024-02-20 20:24:33 +01:00
Valentin271
b0314c5731 chore: remove conventional commit check for PR (#950)
This removes conventional commit check for PRs.

Since we use the PR title and description this is useless. It fails a
lot of time and we ignore it.

IMPORTANT NOTE: This does **not** mean Ratatui abandons conventional
commits. This only relates to commits in PRs.
2024-02-14 19:08:36 +01:00
Dheepak Krishnamurthy
12f67e810f feat: impl Widget for &str and String (#952)
Currently, `f.render_widget("hello world".bold(), area)` works but
`f.render_widget("hello world", area)` doesn't. This PR changes that my
implementing `Widget` for `&str` and `String`. This makes it easier to
render strings with no styles as widgets.

Example usage:

```rust
terminal.draw(|f| f.render_widget("Hello World!", f.size()))?;
```

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-02-14 06:26:08 -05:00
EdJoPaTo
11b452d56f feat(layout): mark various functions as const (#951) 2024-02-13 19:05:23 -08:00
Orhun Parmaksız
efd1e47642 chore(release): prepare for 0.26.1 (#945)
🐭
2024-02-12 12:35:48 +01:00
Orhun Parmaksız
410d08b2b5 docs: add link to FOSDEM 2024 talk (#944) 2024-02-12 10:54:05 +01:00
Orhun Parmaksız
a4892ad444 chore: fix typo in docsrs example (#946) 2024-02-12 10:53:56 +01:00
Orhun Parmaksız
18870ce990 chore: fix the method name for setting the Line style (#947) 2024-02-12 10:53:46 +01:00
Orhun Parmaksız
1f208ffd03 docs: add GitHub Sponsors badge (#943) 2024-02-11 10:54:42 +01:00
Josh McKinney
e51ca6e0d2 refactor: finish tidying up table (#942) 2024-02-11 10:54:08 +01:00
Josh McKinney
9182f47026 feat: add Block::title_top and Block::title_top_bottom (#940)
This adds the ability to add titles to the top and bottom of a block
without having to use the `Title` struct (which will be removed in a
future release - likely v0.28.0).

Fixes a subtle bug if the title was created from a right aligned Line
and was also right aligned. The title would be rendered one cell too far
to the right.

```rust
Block::bordered()
    .title_top(Line::raw("A").left_aligned())
    .title_top(Line::raw("B").centered())
    .title_top(Line::raw("C").right_aligned())
    .title_bottom(Line::raw("D").left_aligned())
    .title_bottom(Line::raw("E").centered())
    .title_bottom(Line::raw("F").right_aligned())
    .render(buffer.area, &mut buffer);
// renders
"┌A─────B─────C┐",
"│             │",
"└D─────E─────F┘",
```

Addresses part of https://github.com/ratatui-org/ratatui/issues/738

<!-- Please read CONTRIBUTING.md before submitting any pull request. -->
2024-02-09 12:50:56 -08:00
Josh McKinney
91040c0865 refactor: rearrange block structure (#939) 2024-02-09 01:13:14 -08:00
Josh McKinney
2202059259 fix(block): fix crash on empty right aligned title (#933)
- Simplified implementation of the rendering for block.
- Introduces a subtle rendering change where centered titles that are
  odd in length will now be rendered one character to the left compared
  to before. This aligns with other places that we render centered text
  and is a more consistent behavior. See
  https://github.com/ratatui-org/ratatui/pull/807#discussion_r1455645954
  for another example of this.

Fixes: https://github.com/ratatui-org/ratatui/pull/929
2024-02-07 15:24:14 -08:00
Dheepak Krishnamurthy
8fb46301a0 chore: Remove github action bot that makes comments nudging commit signing (#937)
We can consider reverting this commit once this PR is merged:
https://github.com/1Password/check-signed-commits-action/pull/9
2024-02-07 19:51:30 +01:00
may
0dcdbea083 fix(paragraph): render Line::styled correctly inside a paragraph (#930)
Renders the styled graphemes of the line instead of the contained spans.
2024-02-06 09:42:17 -08:00
Josh McKinney
74a051147a feat(rect): add Rect::positions iterator (#928)
Useful for performing some action on all the cells in a particular area.
E.g.,

```rust
fn render(area: Rect, buf: &mut Buffer) {
   for position in area.positions() {
        buf.get_mut(position.x, position.y).set_symbol("x");
    }
}
```
2024-02-06 09:39:17 -08:00
Josh McKinney
c3fb25898f refactor(rect): move iters to module and add docs (#927) 2024-02-05 20:25:08 -08:00
Josh McKinney
fae5862c6e fix: ensure that buffer::set_line sets the line style (#926)
Fixes a regression in 0.26 where buffer::set_line was no longer setting
the style. This was due to the new style field on Line instead of being
stored only in the spans.

Also adds a configuration for just running unit tests to bacon.toml.
2024-02-05 16:26:23 -08:00
dependabot[bot]
788e6d9fb8 chore(deps): bump codecov/codecov-action from 3 to 4 (#923)
Bumps
[codecov/codecov-action](https://github.com/codecov/codecov-action) from
3 to 4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/codecov/codecov-action/releases">codecov/codecov-action's
releases</a>.</em></p>
<blockquote>
<h2>v4.0.0</h2>
<p>v4 of the Codecov Action uses the <a
href="https://docs.codecov.com/docs/the-codecov-cli">CLI</a> as the
underlying upload. The CLI has helped to power new features including
local upload, the global upload token, and new upcoming features.</p>
<h2>Breaking Changes</h2>
<ul>
<li>The Codecov Action runs as a <code>node20</code> action due to
<code>node16</code> deprecation. See <a
href="https://github.blog/changelog/2023-09-22-github-actions-transitioning-from-node-16-to-node-20/">this
post from GitHub</a> on how to migrate.</li>
<li>Tokenless uploading is unsupported. However, PRs made from forks to
the upstream public repos will support tokenless (e.g. contributors to
OS projects do not need the upstream repo's Codecov token). This <a
href="https://docs.codecov.com/docs/adding-the-codecov-token#github-actions">doc</a>
shows instructions on how to add the Codecov token.</li>
<li>OS platforms have been added, though some may not be automatically
detected. To see a list of platforms, see our <a
href="https://cli.codecov.io">CLI download page</a></li>
<li>Various arguments to the Action have been changed. Please be aware
that the arguments match with the CLI's needs</li>
</ul>
<p><code>v3</code> versions and below will not have access to CLI
features (e.g. global upload token, ATS).</p>
<h2>What's Changed</h2>
<ul>
<li>build(deps): bump openpgp from 5.8.0 to 5.9.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/985">codecov/codecov-action#985</a></li>
<li>build(deps): bump actions/checkout from 3.0.0 to 3.5.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1000">codecov/codecov-action#1000</a></li>
<li>build(deps): bump ossf/scorecard-action from 2.1.3 to 2.2.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1006">codecov/codecov-action#1006</a></li>
<li>build(deps): bump tough-cookie from 4.0.0 to 4.1.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1013">codecov/codecov-action#1013</a></li>
<li>build(deps-dev): bump word-wrap from 1.2.3 to 1.2.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1024">codecov/codecov-action#1024</a></li>
<li>build(deps): bump node-fetch from 3.3.1 to 3.3.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1031">codecov/codecov-action#1031</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 20.1.4 to
20.4.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1032">codecov/codecov-action#1032</a></li>
<li>build(deps): bump github/codeql-action from 1.0.26 to 2.21.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1033">codecov/codecov-action#1033</a></li>
<li>build commit,report and upload args based on codecovcli by <a
href="https://github.com/dana-yaish"><code>@​dana-yaish</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/943">codecov/codecov-action#943</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 20.4.5 to
20.5.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1055">codecov/codecov-action#1055</a></li>
<li>build(deps): bump github/codeql-action from 2.21.2 to 2.21.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1051">codecov/codecov-action#1051</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 20.5.3 to
20.5.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1058">codecov/codecov-action#1058</a></li>
<li>chore(deps): update outdated deps by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1059">codecov/codecov-action#1059</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 20.5.4 to
20.5.6 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1060">codecov/codecov-action#1060</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from
6.4.1 to 6.5.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1065">codecov/codecov-action#1065</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code>
from 6.4.1 to 6.5.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1064">codecov/codecov-action#1064</a></li>
<li>build(deps): bump actions/checkout from 3.5.3 to 3.6.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1063">codecov/codecov-action#1063</a></li>
<li>build(deps-dev): bump eslint from 8.47.0 to 8.48.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1061">codecov/codecov-action#1061</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 20.5.6 to
20.5.7 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1062">codecov/codecov-action#1062</a></li>
<li>build(deps): bump openpgp from 5.9.0 to 5.10.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1066">codecov/codecov-action#1066</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 20.5.7 to
20.5.9 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1070">codecov/codecov-action#1070</a></li>
<li>build(deps): bump github/codeql-action from 2.21.4 to 2.21.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1069">codecov/codecov-action#1069</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code>
from 6.5.0 to 6.6.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1072">codecov/codecov-action#1072</a></li>
<li>Update README.md by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1073">codecov/codecov-action#1073</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from
6.5.0 to 6.6.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1071">codecov/codecov-action#1071</a></li>
<li>build(deps-dev): bump <code>@​vercel/ncc</code> from 0.36.1 to
0.38.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1074">codecov/codecov-action#1074</a></li>
<li>build(deps): bump <code>@​actions/core</code> from 1.10.0 to 1.10.1
by <a href="https://github.com/dependabot"><code>@​dependabot</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1081">codecov/codecov-action#1081</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code>
from 6.6.0 to 6.7.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1080">codecov/codecov-action#1080</a></li>
<li>build(deps): bump actions/checkout from 3.6.0 to 4.0.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1078">codecov/codecov-action#1078</a></li>
<li>build(deps): bump actions/upload-artifact from 3.1.2 to 3.1.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1077">codecov/codecov-action#1077</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 20.5.9 to
20.6.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1075">codecov/codecov-action#1075</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from
6.6.0 to 6.7.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1079">codecov/codecov-action#1079</a></li>
<li>build(deps-dev): bump eslint from 8.48.0 to 8.49.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1076">codecov/codecov-action#1076</a></li>
<li>use cli instead of node uploader by <a
href="https://github.com/dana-yaish"><code>@​dana-yaish</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1068">codecov/codecov-action#1068</a></li>
<li>chore(release): 4.0.0-beta.1 by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1084">codecov/codecov-action#1084</a></li>
<li>not adding -n if empty to do-upload command by <a
href="https://github.com/dana-yaish"><code>@​dana-yaish</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1085">codecov/codecov-action#1085</a></li>
<li>4.0.0-beta.2 by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1086">codecov/codecov-action#1086</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md">codecov/codecov-action's
changelog</a>.</em></p>
<blockquote>
<h2>4.0.0-beta.2</h2>
<h3>Fixes</h3>
<ul>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/1085">#1085</a>
not adding -n if empty to do-upload command</li>
</ul>
<h2>4.0.0-beta.1</h2>
<p><code>v4</code> represents a move from the <a
href="https://github.com/codecov/uploader">universal uploader</a> to the
<a href="https://github.com/codecov/codecov-cli">Codecov CLI</a>.
Although this will unlock new features for our users, the CLI is not yet
at feature parity with the universal uploader.</p>
<h3>Breaking Changes</h3>
<ul>
<li>No current support for <code>aarch64</code> and <code>alpine</code>
architectures.</li>
<li>Tokenless uploading is unsuported</li>
<li>Various arguments to the Action have been removed</li>
</ul>
<h2>3.1.4</h2>
<h3>Fixes</h3>
<ul>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/967">#967</a>
Fix typo in README.md</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/971">#971</a>
fix: add back in working dir</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/969">#969</a>
fix: CLI option names for uploader</li>
</ul>
<h3>Dependencies</h3>
<ul>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/970">#970</a>
build(deps-dev): bump <code>@​types/node</code> from 18.15.12 to
18.16.3</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/979">#979</a>
build(deps-dev): bump <code>@​types/node</code> from 20.1.0 to
20.1.2</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/981">#981</a>
build(deps-dev): bump <code>@​types/node</code> from 20.1.2 to
20.1.4</li>
</ul>
<h2>3.1.3</h2>
<h3>Fixes</h3>
<ul>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/960">#960</a>
fix: allow for aarch64 build</li>
</ul>
<h3>Dependencies</h3>
<ul>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/957">#957</a>
build(deps-dev): bump jest-junit from 15.0.0 to 16.0.0</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/958">#958</a>
build(deps): bump openpgp from 5.7.0 to 5.8.0</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/959">#959</a>
build(deps-dev): bump <code>@​types/node</code> from 18.15.10 to
18.15.12</li>
</ul>
<h2>3.1.2</h2>
<h3>Fixes</h3>
<ul>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/718">#718</a>
Update README.md</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/851">#851</a>
Remove unsupported path_to_write_report argument</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/898">#898</a>
codeql-analysis.yml</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/901">#901</a>
Update README to contain correct information - inputs and negate
feature</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/955">#955</a>
fix: add in all the extra arguments for uploader</li>
</ul>
<h3>Dependencies</h3>
<ul>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/819">#819</a>
build(deps): bump openpgp from 5.4.0 to 5.5.0</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/835">#835</a>
build(deps): bump node-fetch from 3.2.4 to 3.2.10</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/840">#840</a>
build(deps): bump ossf/scorecard-action from 1.1.1 to 2.0.4</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/841">#841</a>
build(deps): bump <code>@​actions/core</code> from 1.9.1 to 1.10.0</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/843">#843</a>
build(deps): bump <code>@​actions/github</code> from 5.0.3 to 5.1.1</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/869">#869</a>
build(deps): bump node-fetch from 3.2.10 to 3.3.0</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/872">#872</a>
build(deps-dev): bump jest-junit from 13.2.0 to 15.0.0</li>
<li><a
href="https://redirect.github.com/codecov/codecov-action/issues/879">#879</a>
build(deps): bump decode-uri-component from 0.2.0 to 0.2.2</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="e0b68c6749"><code>e0b68c6</code></a>
fix: show both token uses in readme (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1250">#1250</a>)</li>
<li><a
href="1f9f5573d1"><code>1f9f557</code></a>
Add all args (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1245">#1245</a>)</li>
<li><a
href="09686fcfcb"><code>09686fc</code></a>
Update README.md (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1243">#1243</a>)</li>
<li><a
href="f30e4959ba"><code>f30e495</code></a>
fix: update action.yml (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1240">#1240</a>)</li>
<li><a
href="a7b945cea4"><code>a7b945c</code></a>
fix: allow for other archs (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1239">#1239</a>)</li>
<li><a
href="98ab2c591b"><code>98ab2c5</code></a>
Update package.json (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1238">#1238</a>)</li>
<li><a
href="43235cc5ae"><code>43235cc</code></a>
Update README.md (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1237">#1237</a>)</li>
<li><a
href="0cf8684c82"><code>0cf8684</code></a>
chore(ci): bump to node20 (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1236">#1236</a>)</li>
<li><a
href="8e1e730371"><code>8e1e730</code></a>
build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code>
from 6.19.1 to 6.20.0 ...</li>
<li><a
href="61293af0e8"><code>61293af</code></a>
build(deps-dev): bump <code>@​typescript-eslint/parser</code> from
6.19.1 to 6.20.0 (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1235">#1235</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/codecov/codecov-action/compare/v3...v4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=codecov/codecov-action&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-02-05 16:25:33 -08:00
Jack Wills
14c67fbb52 fix(list): highlight symbol when using a multi-bytes char (#924)
ratatui v0.26.0 brought a regression in the List widget, in which the
highlight symbol width was incorrectly calculated - specifically when
the highlight symbol was a multi-char character, e.g. `▶`.
2024-02-05 20:59:19 +01:00
Mo
096346350e perf: Use drain instead of remove in chart examples (#922) 2024-02-05 04:54:05 -08:00
Valentin271
61a827821d docs(canvas): add documentation to canvas module (#913)
Document the whole `canvas` module. With this, the whole `widgets`
module is documented.
2024-02-03 13:58:29 -08:00
Dheepak Krishnamurthy
fbb5dfaaa9 fix: Scrollbar rendering when no track symbols are provided (#911) 2024-02-03 16:06:44 +01:00
Orhun Parmaksız
d2d91f754c docs(changelog): add sponsors section (#908)
<!-- Please read CONTRIBUTING.md before submitting any pull request. -->
2024-02-02 17:28:23 +01:00
Dheepak Krishnamurthy
bcf43688ec docs: Update BREAKING-CHANGES.md summary (#907)
Update summary section
2024-02-02 03:50:07 -05:00
Orhun Parmaksız
b7942ee252 chore(release): prepare for 0.26.0 (#905)
🐭

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-02-02 00:16:00 -08:00
Josh McKinney
c8dd87918d feat: add WidgetRef and StatefulWidgetRef traits (#903)
The Widget trait consumes self, which makes it impossible to use in a
boxed context. Previously we implemented the Widget trait for &T, but
this was not enough to render a boxed widget. We now have a new trait
called `WidgetRef` that allows rendering a widget by reference. This
trait is useful when you want to store a reference to one or more
widgets and render them later. Additionaly this makes it possible to
render boxed widgets where the type is not known at compile time (e.g.
in a composite layout with multiple panes of different types).

This change also adds a new trait called `StatefulWidgetRef` which is
the stateful equivalent of `WidgetRef`.

Both new traits are gated behind the `unstable-widget-ref` feature flag
as we may change the exact name / approach a little on this based on
further discussion.

Blanket implementation of `Widget` for `&W` where `W` implements
`WidgetRef` and `StatefulWidget` for `&W` where `W` implements
`StatefulWidgetRef` is provided. This allows you to render a widget by
reference and a stateful widget by reference.

A blanket implementation of `WidgetRef` for `Option<W>` where `W`
implements `WidgetRef` is provided. This makes it easier to render
child widgets that are optional without the boilerplate of unwrapping
the option. Previously several widgets implemented this manually. This
commits expands the pattern to apply to all widgets.

```rust
struct Parent {
    child: Option<Child>,
}

impl WidgetRef for Parent {
    fn render_ref(&self, area: Rect, buf: &mut Buffer) {
        self.child.render_ref(area, buf);
    }
}
```

```rust
let widgets: Vec<Box<dyn WidgetRef>> = vec![Box::new(Greeting), Box::new(Farewell)];
for widget in widgets {
    widget.render_ref(buf.area, &mut buf);
}
assert_eq!(buf, Buffer::with_lines(["Hello        Goodbye"]));
```
2024-02-02 00:02:16 -08:00
Dheepak Krishnamurthy
652dc469ea docs: Update BREAKING-CHANGES.md with flex section (#906)
Follow up to: https://github.com/ratatui-org/ratatui/pull/881
2024-02-02 00:40:11 -05:00
Josh McKinney
87bf1dd9df feat: replace Rect::split with Layout::areas and spacers (#904)
In a recent commit we added Rec::split, but this feels more ergonomic as
Layout::areas. This also adds Layout::spacers to get the spacers between
the areas.
2024-02-01 20:26:35 -08:00
Josh McKinney
f8367fdfdd chore: allow Buffer::with_lines to accept IntoIterator (#901)
This can make it easier to use `Buffer::with_lines` with iterators that
don't necessarily produce a `Vec`. For example, this allows using
`Buffer::with_lines` with `&[&str]` directly, without having to call
`collect` on it first.
2024-01-31 14:12:10 -08:00
Josh McKinney
9ba7354335 feat(text): implement iterators for Text (#900)
This allows iterating over the `Lines`s of a text using `for` loops and
other iterator methods.

- add `iter` and `iter_mut` methods to `Text`
- implement `IntoIterator` for `Text`, `&Text`, and `&mut Text` traits
- update call sites to iterate over `Text` rather than `Text::lines`
2024-01-31 13:44:39 -08:00
Dheepak Krishnamurthy
dab08b99b6 feat: show space constrained UIs conditionally (#895)
With this PR the constraint explorer demo only shows space constrained
UIs instead:

Smallest (15 row height):

<img width="759" alt="image"
src="https://github.com/ratatui-org/ratatui/assets/1813121/37a4a027-6c6d-4feb-8104-d732aee298ac">

Small (20 row height):

<img width="759" alt="image"
src="https://github.com/ratatui-org/ratatui/assets/1813121/f76e025f-0061-4f09-9c91-2f7b00fcfb9e">

Medium (30 row height):

<img width="758" alt="image"
src="https://github.com/ratatui-org/ratatui/assets/1813121/81b070da-1bfb-40c5-9fbc-c1ab44ce422e">

Full (40 row height):

<img width="760" alt="image"
src="https://github.com/ratatui-org/ratatui/assets/1813121/7bb8a8c4-1a77-4bbc-a346-c8b5c198c6d3">
2024-01-31 13:01:29 -08:00
Josh McKinney
2a12f7bddf feat: impl Widget for &BarChart (#897)
BarChart had some internal mutations that needed to be removed to
implement the Widget trait for &BarChart to bring it in line with the
other widgets.
2024-01-31 11:21:32 -08:00
Josh McKinney
4278b4088d feat(line): implement iterators for Line (#896)
This allows iterating over the `Span`s of a line using `for` loops and
other iterator methods.

- add `iter` and `iter_mut` methods to `Line`
- implement `IntoIterator` for `Line`, `&Line`, and `&mut Line` traits
- update call sites to iterate over `Line` rather than `Line::spans`
2024-01-31 17:33:30 +01:00
Dheepak Krishnamurthy
86168aa711 docs: Fix docstring for Max constraints (#898) 2024-01-31 17:31:05 +01:00
Josh McKinney
78f1c1446b chore: small fixes to constraint-explorer (#894) 2024-01-30 21:27:56 -08:00
Dheepak Krishnamurthy
9ec43eff1c feat: Constraint Explorer example (#893)
Here's a constraint explorer demo put together with @joshka 


https://github.com/ratatui-org/ratatui/assets/1813121/08d7d8f6-d013-44b4-8331-f4eee3589cce

It allows users to interactive explore how the constraints behave with
respect to each other and compare that across flex modes. It allows
users to swap constraints out for other constraints, increment or
decrement the values, add and remove constraints, and add spacing

It is also a good example for how to structure a simple TUI with several
Ratatui code patterns that are useful for refactoring.

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

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-01-31 00:12:29 -05:00
Dheepak Krishnamurthy
4ee4e6d78a feat: Make spacing work in Flex::SpaceAround and Flex::SpaceBetween (#892)
This PR implements user provided spacing gaps for `SpaceAround` and
`SpaceBetween`.


https://github.com/ratatui-org/ratatui/assets/1813121/2e260708-e8a7-48ef-aec7-9cf84b655e91

Now user provided spacing gaps always take priority in all `Flex` modes.
2024-01-30 23:34:59 -05:00
Josh McKinney
525479546a refactor: make layout tests a bit easier to understand (#890) 2024-01-29 23:07:18 -08:00
Dheepak Krishnamurthy
cf861232c7 refactor(Scrollbar): Rewrite scrollbar implementation (#847)
Implementation was simplified and calculates the size of the thumb a
bit more proportionally to the content that is visible.

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-01-29 23:05:24 -08:00
Dheepak Krishnamurthy
dd5ca3a0c8 feat: Better weights for constraints (#889)
This PR is a split of reworking the weights from #888 

This keeps the same ranking of weights, just uses a different numerical
value so that the lowest weight is `WEAK` (`1.0`).

No tests are changed as a result of this change, and running the
following multiple times did not cause any errors for me:

```rust
for i in {0..100}
do
 cargo test --lib --
 if [ $? -ne 0 ]; then
 echo "Test failed. Exiting loop."
 break
 fi
done
```
2024-01-29 22:16:49 -08:00
Dheepak Krishnamurthy
aeec16369b feat: Change rounding to make tests stable (#888)
This fixes some unstable tests
2024-01-30 00:33:33 -05:00
dependabot[bot]
eb31617539 chore(deps): update termwiz requirement from 0.20.0 to 0.22.0 (#885)
Updates the requirements on [termwiz](https://github.com/wez/wezterm) to
permit the latest version.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/wez/wezterm/commits">compare view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-29 20:02:44 -08:00
dependabot[bot]
1ad629acd7 chore(deps): update derive_builder requirement from 0.12.0 to 0.13.0 (#887) 2024-01-29 17:12:43 +01:00
dependabot[bot]
04e6ccd448 chore(deps): update strum requirement from 0.25 to 0.26 (#886) 2024-01-29 16:59:42 +01:00
Dheepak Krishnamurthy
540fd2df03 feat(layout)!: Change Flex::default() (#881)
This PR makes a number of simplifications to the layout and constraint
features that were added after v0.25.0.

For users upgrading from v0.25.0, the net effect of this PR (along with
the other PRs) is the following:

- New `Flex` modes have been added.
  - `Flex::Start` (new default)
  - `Flex::Center`
  - `Flex::End`
  - `Flex::SpaceAround`
  - `Flex::SpaceBetween`
  - `Flex::Legacy` (old default)
- `Min(v)` grows to allocate excess space in all `Flex` modes instead of
shrinking (except in `Flex::Legacy` where it retains old behavior).
- `Fill(1)` grows to allocate excess space, growing equally with
`Min(v)`.

---

The following contains a summary of the changes in this PR and the
motivation behind them.

**`Flex`**

- Removes `Flex::Stretch`
- Renames `Flex::StretchLast` to `Flex::Legacy`

**`Constraint`**

- Removes `Fixed`
- Makes `Min(v)` grow as much as possible everywhere (except
`Flex::Legacy` where it retains the old behavior)
- Makes `Min(v)` grow equally as `Fill(1)` while respecting `Min` lower
bounds. When `Fill` and `Min` are used together, they both fill excess
space equally.

Allowing `Min(v)` to grow still allows users to build the same layouts
as before with `Flex::Start` with no breaking changes to the behavior.

This PR also removes the unstable feature `SegmentSize`.

This is a breaking change to the behavior of constraints. If users want
old behavior, they can use `Flex::Legacy`.

```rust
Layout::vertical([Length(25), Length(25)]).flex(Flex::Legacy)
```

Users that have constraint that exceed the available space will probably
not see any difference or see an improvement in their layouts. Any
layout with `Min` will be identical in `Flex::Start` and `Flex::Legacy`
so any layout with `Min` will not be breaking.

Previously, `Table` used `EvenDistribution` internally by default, but
with that gone the default is now `Flex::Start`. This changes the
behavior of `Table` (for the better in most cases). The only way for
users to get exactly the same as the old behavior is to change their
constraints. I imagine most users will be happier out of the box with
the new Table default.

Resolves https://github.com/ratatui-org/ratatui/issues/843

Thanks to @joshka for the direction
2024-01-29 09:37:50 -05:00
Josh McKinney
984afd580b chore: cache dependencies in the CI workflow to speed up builds (#883) 2024-01-29 14:43:48 +01:00
Josh McKinney
bbcfa55a88 feat(layout): add Rect::contains method (#882)
This is useful for performing hit tests (i.e. did the user click in an
area).
2024-01-29 04:44:22 -08:00
Dheepak Krishnamurthy
1cbe1f52ab feat(constraints): Rename Constraint::Proportional to Constraint::Fill (#880)
`Constraint::Fill` is a more intuitive name for the behavior, and it is
shorter.

Resolves #859
2024-01-28 05:41:01 -05:00
Dheepak Krishnamurthy
663bbde9c3 test(layout): Convert layout tests to use rstest (#879)
This PR makes all the letters test use `rstest`
2024-01-28 05:11:40 -05:00
Dheepak Krishnamurthy
27e9216cea feat(table): Remove allow deprecated attribute used previously for segment_size (#875) 2024-01-27 13:45:55 -08:00
Dheepak Krishnamurthy
be4fdaa0c7 feat: Change priority of constraints and add split_with_spacers (#788)
Follow up to https://github.com/ratatui-org/ratatui/pull/783

This PR introduces different priorities for each kind of constraint.
This PR also adds tests that specifies this behavior. This PR resolves a
number of broken tests.

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

With this PR, the layout algorithm will do the following in order:

1. Ensure that all the segments are within the user provided area and
ensure that all segments and spacers are aligned next to each other
2. if a user provides a `layout.spacing`, it will enforce it.
3. ensure proportional elements are all proportional to each other
4. if a user provides a `Fixed(v)` constraint, it will enforce it. 
5. `Min` / `Max` binding inequality constraints
6. `Length`
7. `Percentage`
8. `Ratio`
9. collapse `Min` or collapse `Max`
10. grow `Proportional` as much as possible
11. grow spacers as much as possible

This PR also returns the spacer areas as `Rects` to the user. Users can
then draw into the spacers as they see fit (thanks @joshka for the
idea). Here's a screenshot with the modified flex example:

<img width="569" alt="image"
src="https://github.com/ratatui-org/ratatui/assets/1813121/46c8901d-882c-43b0-ba87-b1d455099d8f">

This PR introduces a `strengths` module that has "default" weights that
give stable solutions as well as predictable behavior.
2024-01-27 15:35:42 -05:00
Eeelco
c1ed5c3637 feat(Span): add alignment functions (#873)
Implemented functions that convert Span into a
left-/center-/right-aligned Line. Implemented unit tests.

Closes #853 
---------

Signed-off-by: Eelco Empting <me@eelco.de>
2024-01-27 10:57:06 -08:00
Emirhan TALA
4b8e54e811 docs(examples): refactor Tabs example (#861)
- Used a few new techniques from the 0.26 features (ref widgets, text rendering,
  dividers / padding etc.)
- Updated the app to a simpler application approach
- Use color_eyre
- Make it look pretty (colors, new proportional borders)

![Made with VHS](https://vhs.charm.sh/vhs-4WW21XTtepDhUSq4ZShO56.gif)

---------
Fixes https://github.com/ratatui-org/ratatui/issues/819
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-01-27 02:39:40 -08:00
Emirhan TALA
5b7ad2ad82 docs(examples): update gauge example (#863)
- colored gauges
- removed box borders
- show the difference between ratio / percentage and unicode / no unicode better
- better application approach (consistent with newer examples)
- various changes for 0.26 featuers
- impl `Widget` for `&App`
- use color_eyre

for gauge.tape

- change to get better output from the new code

---------
Fixes: https://github.com/ratatui-org/ratatui/issues/846
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-01-27 01:47:17 -08:00
Valentin271
ba20372c23 chore(CONTRIBUTING): remove part about squashing commits (#874)
Removes the part about squashing commits from the CONTRIBUTING file.

We no longer require that because github squashes commits when merging.
This will cleanup the CONTRIBUTING file a bit which is already quite
dense.
2024-01-26 18:30:15 +01:00
Josh McKinney
f383625f0e docs(examples): add note about example versions to all examples (#871) 2024-01-24 11:50:18 -08:00
Josh McKinney
847bacf32e docs(examples): refactor demo2 (#836)
Simplified a bunch of the logic in the demo2 example
- Moved destroy mode to its own file.
- Moved error handling to its own file.
- Removed AppContext
- Implemented Widget for &App. The app state is small enough that it
  doesn't matter here and we could just copy or clone the app state on
  every frame, but for larger apps this can be a significant performance
  improvement.
- Made the tabs stateful
- Made the term module just a collection of functions rather than a
  struct.
- Changed to use color_eyre for error handling.
- Changed keyboard shortcuts and rearranged the bottom bar.
- Use strum for the tabs enum.
2024-01-24 11:44:16 -08:00
Josh McKinney
815757fcbb feat(widgets): implement Widget for Widget refs (#833)
Many widgets can be rendered without changing their state.

This commit implements The `Widget` trait for references to
widgets and changes their implementations to be immutable.

This allows us to render widgets without consuming them by passing a ref
to the widget when calling `Frame::render_widget()`.

```rust
// this might be stored in a struct
let paragraph = Paragraph::new("Hello world!");

let [left, right] = area.split(&Layout::horizontal([20, 20]));
frame.render_widget(&paragraph, left);
frame.render_widget(&paragraph, right); // we can reuse the widget
```

Implemented for all widgets except BarChart (which has an implementation
that modifies the internal state and requires a rewrite to fix.

Other widgets will be implemented in follow up commits.

Fixes: https://github.com/ratatui-org/ratatui/discussions/164
Replaces PRs: https://github.com/ratatui-org/ratatui/pull/122 and
https://github.com/ratatui-org/ratatui/pull/16
Enables: https://github.com/ratatui-org/ratatui/issues/132
Validated as a viable working solution by:
https://github.com/ratatui-org/ratatui/pull/836
2024-01-24 10:34:10 -08:00
Josh McKinney
736605ec88 feat(layout): Add default impl for Position (#869) 2024-01-24 17:47:29 +01:00
Josh McKinney
6e76729ce8 chore: move example vhs tapes to a folder (#867) 2024-01-24 08:22:46 -08:00
Josh McKinney
7f42ec9713 refactor(colors_rgb): impl widget on mutable refs (#865)
This commit refactors the colors_rgb example to implement the Widget
trait on mutable references to the app and its sub-widgets. This allows
the app to update its state while it is being rendered.

Additionally the main and run functions are refactored to be similar to
the other recent examples. This uses a pattern where the App struct has
a `run` method that takes a terminal as an argument, and the main
function is in control of initializing and restoring the terminal and
installing the error hooks.
2024-01-24 07:13:11 -08:00
Dheepak Krishnamurthy
d7132011f9 feat: Add Color::from_hsl (#772)
This PR adds `Color::from_hsl` that returns a valid `Color::Rgb`. 

```rust
let color: Color = Color::from_hsl(360.0, 100.0, 100.0);
assert_eq!(color, Color::Rgb(255, 255, 255));

let color: Color = Color::from_hsl(0.0, 0.0, 0.0);
assert_eq!(color, Color::Rgb(0, 0, 0));
```

HSL stands for Hue (0-360 deg), Saturation (0-100%), and Lightness
(0-100%) and working with HSL the values can be more intuitive. For
example, if you want to make a red color more orange, you can change the
Hue closer toward yellow on the color wheel (i.e. increase the Hue).

Related #763
2024-01-24 04:42:39 -08:00
Eeelco
d726e928d2 feat(Paragraph): add alignment convenience functions (#866)
Added convenience functions left_aligned(), centered() and
right_aligned() plus unit tests. Updated example code.

Signed-off-by: Eelco Empting <me@eelco.de>
2024-01-24 03:31:52 -08:00
Eeelco
b80264de87 feat(Text): add alignment convenience functions (#862)
Adds convenience functions `left_aligned()`, `centered()` and
`right_aligned()` plus unit tests.
2024-01-23 20:28:38 +01:00
Emirhan TALA
804c841fdc docs(examples): update list example and list.tape (#864)
This PR adds:

- subjectively better-looking list example
- change list example to a todo list example
- status of a TODO can be changed, further info can be seen under the list.
2024-01-23 17:22:37 +01:00
Eeelco
79ceb9f7b6 feat(Line): add alignment convenience functions (#856)
This adds convenience functions `left_aligned()`, `centered()` and
`right_aligned()` plus unit tests. Updated example code.
2024-01-22 18:30:01 +01:00
Dheepak Krishnamurthy
f780be31f3 test(layout): parameterized tests 🚨 (#858) 2024-01-21 10:37:17 -05:00
Emirhan TALA
eb1484b6db docs(examples): update tabs example and tabs.tape (#855)
This PR adds:

for tabs.rs

- general refactoring on code
- subjectively better looking front
- add tailwind colors

for tabs.tape

- change to get better output from the new code

Here is the new output:

![tabs](https://github.com/ratatui-org/ratatui/assets/30180366/0a9371a5-e90d-42ba-aba5-70cbf66afd1f)
2024-01-21 10:23:50 +01:00
Emirhan TALA
6ecaeed549 docs(Text): add overview of the relevant methods (#857)
Add an overview of the relevant methods under `Constructor Methods`, `Setter Methods`, and `Other Methods` subtitles.
2024-01-20 23:32:33 +01:00
Emirhan TALA
a489d85f2d feat(table): deprecate SegmentSize on table (#842)
This adds for table:

- Added new flex method with flex field
- Deprecated segment_size method and removed segment_size field
- Updated documentation
- Updated tests
2024-01-19 17:58:12 +01:00
Valentin271
065b6b05b7 docs(terminal): document buffer diffing better (#852) 2024-01-19 17:44:50 +01:00
Josh McKinney
1e755967c5 feat(layout): increase default cache size to 500 (#850)
This is a somewhat arbitrary size for the layout cache based on adding
the columns and rows on my laptop's terminal (171+51 = 222) and doubling
it for good measure and then adding a bit more to make it a round
number. This gives enough entries to store a layout for every row and
every column, twice over, which should be enough for most apps. For
those that need more, the cache size can be set with
`Layout::init_cache()`.

Fixes: https://github.com/ratatui-org/ratatui/issues/820
2024-01-19 15:35:33 +01:00
Linda_pp
1d3fbc1b15 perf(buffer)!: apply SSO technique to text buffer in buffer::Cell (#601)
Use CompactString instead of String to store the Cell::symbol field.
This saves reduces the size of memory allocations at runtime.
2024-01-19 04:50:42 -08:00
Emirhan TALA
330a899eac docs(examples): update table example and table.tape (#840)
In table.rs
- added scrollbar to the table
- colors changed to use style::palette::tailwind
- now colors can be changed with keys (l or →) for the next color, (h or
←) for the previous color
- added a footer for key info

For table.tape
- typing speed changed to 0.75s from 0.5s
- screen size changed to fit
- pushed keys changed to show the current example better

Fixes: https://github.com/ratatui-org/ratatui/issues/800
2024-01-19 03:26:09 -08:00
Emirhan TALA
405a125c82 feat: add wide and tall proportional border set (#848)
Adds `PROPORTIONAL_WIDE` and `PROPORTIONAL_TALL` border sets.

`symbols::border::PROPORTIONAL_WIDE`
```
▄▄▄▄
█xx█
█xx█
▀▀▀▀
```

`symbols::border::PROPORTIONAL_TALL`
```
█▀▀█
█xx█
█xx█
█▄▄█
```

Fixes: https://github.com/ratatui-org/ratatui/issues/834
2024-01-19 00:24:49 -08:00
bblsh
b3a57f3dff fix(list): Modify List and List example to support saving offsets. (#667)
The current `List` example will unselect and reset the position of a
list.

This PR will save the last selected item, and updates `List` to honor
its offset, preventing the list from resetting when the user
`unselect()`s a `StatefulList`.
2024-01-19 09:17:39 +01:00
Josh McKinney
2819eea82b feat(layout): add Position struct (#790)
This stores the x and y coordinates (columns and rows)

- add conversions from Rect
- add conversion with Size to Rect
- add Rect::as_position
2024-01-18 14:07:36 +01:00
Josh McKinney
41de8846fd docs(examples): document incompatible examples better (#844)
Examples often take advantage of unreleased API changes, which makes
them not copy-paste friendly.
2024-01-18 01:56:06 -08:00
Valentin271
68d5783a69 feat(text): add style and alignment (#807)
Fixes #758, fixes #801

This PR adds:

- `style` and `alignment` to `Text`
- impl `Widget` for `Text`
- replace `Text` manual draw to call for Widget impl

All places that use `Text` have been updated and support its new
features expect paragraph which still has a custom implementation.
2024-01-17 18:54:53 +01:00
Orhun Parmaksız
d49bbb2590 chore(ci): update the job description for installing cargo-nextest (#839) 2024-01-17 18:08:06 +01:00
Josh McKinney
fd4703c086 refactor(block): move padding and title into separate files (#837) 2024-01-17 06:18:36 -08:00
Emirhan TALA
0df935473f feat(Padding): add new constructors for padding (#828)
Adds `proportional`, `symmetric`, `left`, `right`, `top`, and `bottom`
constructors for Padding struct.

Proportional is 
```
/// **NOTE**: Terminal cells are often taller than they are wide, so to make horizontal and vertical
/// padding seem equal, doubling the horizontal padding is usually pretty good.
```

Fixes: https://github.com/ratatui-org/ratatui/issues/798
2024-01-17 04:50:08 -08:00
Dheepak Krishnamurthy
9df6cebb58 feat: Table column calculation uses layout spacing (#824)
This uses the new `spacing` feature of the `Layout` struct to allocate
columns spacing in the `Table` widget.
This changes the behavior of the table column layout in the following
ways:

1. Selection width is always allocated.
- if a user does not want a selection width ever they should use
`HighlightSpacing::Never`
2. Column spacing is prioritized over other constraints
- if a user does not want column spacing, they should use
`Table::new(...).column_spacing(0)`

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-01-16 21:51:25 -05:00
Dheepak Krishnamurthy
f299463847 feat: Add one eighth wide and tall border sets (#831)
This PR adds the
[`McGugan`](https://www.willmcgugan.com/blog/tech/post/ceo-just-wants-to-draw-boxes/)
border set, which allows for tighter borders.

For example, with the `flex` example you can get this effect (top is
mcgugan wide, bottom is mcgugan tall):

<img width="759" alt="image"
src="https://github.com/ratatui-org/ratatui/assets/1813121/756bb50e-f8c3-4eec-abe8-ce358058a526">

<img width="759" alt="image"
src="https://github.com/ratatui-org/ratatui/assets/1813121/583485ef-9eb2-4b45-ab88-90bd7cb14c54">

As of this PR, `MCGUGAN_WIDE` has to be styled manually, like so:

```rust
            let main_color = color_for_constraint(*constraint);
            let cell = buf.get_mut(block.x, block.y + 1);
            cell.set_style(Style::reset().fg(main_color).reversed());
            let cell = buf.get_mut(block.x, block.y + 2);
            cell.set_style(Style::reset().fg(main_color).reversed());
            let cell = buf.get_mut(block.x + block.width.saturating_sub(1), block.y + 1);
            cell.set_style(Style::reset().fg(main_color).reversed());
            let cell = buf.get_mut(block.x + block.width.saturating_sub(1), block.y + 2);
            cell.set_style(Style::reset().fg(main_color).reversed());

```

`MCGUGAN_TALL` has to be styled manually, like so:

```rust
            let main_color = color_for_constraint(*constraint);
            for x in block.x + 1..(block.x + block.width).saturating_sub(1) {
                let cell = buf.get_mut(x, block.y);
                cell.set_style(Style::reset().fg(main_color).reversed());
                let cell = buf.get_mut(x, block.y + block.height - 1);
                cell.set_style(Style::reset().fg(main_color).reversed());
            }

```
2024-01-16 21:03:06 -05:00
Josh McKinney
4d262d21cb refactor(widget): move borders to widgets/borders.rs (#832) 2024-01-16 16:16:42 -08:00
Dheepak Krishnamurthy
ae6a2b0007 feat: Add spacing feature to flex example (#830)
This adds the `spacing` using `+` and `-` to the flex example
2024-01-16 17:19:23 +01:00
Dheepak Krishnamurthy
f71bf18297 fix: bug with flex stretch with spacing and proportional constraints (#829)
This PR fixes a bug with layouts when using spacing on proportional
constraints.
2024-01-16 12:35:58 +01:00
Emirhan TALA
cddf4b2930 feat: implement Display for Text, Line, Span (#826)
Issue: https://github.com/ratatui-org/ratatui/issues/816

This PR adds:

`std::fmt::Display` for `Text`, `Line`, and `Span` structs.

Display implementation displays actual content while ignoring style.
2024-01-16 08:52:20 +01:00
Valentin271
813f707892 refactor(example): improve constraints and flex examples (#817)
This PR is a follow up to
https://github.com/ratatui-org/ratatui/pull/811.

It improves the UI of the layouts by

- thoughtful accessible color that represent priority in constraints
resolving
- using QUADRANT_OUTSIDE symbol set for block rendering
- adding a scrollbar
- panic handling
- refactoring for readability

to name a few. Here are some example gifs of the outcome:


![constraints](https://github.com/ratatui-org/ratatui/assets/381361/8eed34cf-e959-472f-961b-d439bfe3324e)


![flex](https://github.com/ratatui-org/ratatui/assets/381361/3195a56c-9cb6-4525-bc1c-b969c0d6a812)

---------

Co-authored-by: Dheepak Krishnamurthy <me@kdheepak.com>
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-01-15 20:56:40 -08:00
Valentin271
48b0380cb3 docs(scrollbar): complete scrollbar documentation (#823) 2024-01-15 18:37:55 +01:00
Absolutex
c959bd2881 fix(calendar): CalendarEventStore panic (#822)
`CalendarEventStore::today()` panics if the system's UTC offset cannot
be determined. In this circumstance, it's better to use `now_utc`
instead.
2024-01-15 17:07:47 +01:00
Dheepak Krishnamurthy
5131c813ce feat: Add layout spacing (#821)
This adds a `spacing` feature for layouts.

Spacing can be added between items of a layout.
2024-01-15 16:39:00 +01:00
Dheepak Krishnamurthy
11e4f6a0ba docs: Adds better documentation for constraints and flex 📚 (#818) 2024-01-14 16:58:58 -05:00
Dheepak Krishnamurthy
cc6737b8bc fix: make SpaceBetween with one element Stretch 🐛 (#813)
When there's just one element, `SpaceBetween` should do the same thing
as `Stretch`.
2024-01-14 18:13:49 +01:00
Valentin271
3e7810a2ab fix(example): increase layout cache size (#815)
This was causing very bad performances especially on scrolling.
It's also a good usage demonstration.
2024-01-14 18:10:12 +01:00
Dheepak Krishnamurthy
fc0879f98d chore(layout): comment tests that may fail on occasion (#814)
These fails seem to fail on occasion, locally and on CI.

This issue will be revisited in the PR on constraint weights:
https://github.com/ratatui-org/ratatui/pull/788
2024-01-14 17:47:48 +01:00
Valentin271
bb5444f618 refactor(example): add scroll to flex example (#811)
This commit adds `scroll` to the flex example. It also adds more examples to showcase how constraints interact. It improves the UI to make it easier to understand and short terminal friendly.

<img width="380" alt="image" src="https://github.com/ratatui-org/ratatui/assets/1813121/30541efc-ecbe-4e28-b4ef-4d5f1dc63fec"/>

---------

Co-authored-by: Dheepak Krishnamurthy <me@kdheepak.com>
2024-01-14 10:49:45 -05:00
Valentin271
e0aa6c5e1f refactor(chart): replace deprecated apply (#812)
Fixes #793
2024-01-14 15:37:34 +01:00
Dheepak Krishnamurthy
1746a61659 docs: Update links to templates repository 📚 (#810)
This PR updates links to the `templates` repository.
2024-01-14 13:05:54 +01:00
Valentin271
7a8af8da6b fix: update templates links (#808) 2024-01-13 15:40:44 -08:00
Josh McKinney
dfd6db988f feat(demo2): add destroy mode to celebrate commit 1000! (#809)
```shell
cargo run --example demo2 --features="crossterm widget-calendar"
```

Press `d` to activate destroy mode and Enjoy!

![Destroy
Demo2](1d39444e3d/examples/demo2-destroy.gif)

Vendors a copy of tui-big-text to allow us to use it in the demo.
2024-01-13 15:13:50 -08:00
Josh McKinney
151db6ac7d chore: add commit footers to git-cliff config (#805)
Fixes: https://github.com/orhun/git-cliff/issues/297
2024-01-13 02:13:09 -08:00
Dheepak Krishnamurthy
de97a1f1da feat: Add flex to layout
This PR adds a new way to space elements in a `Layout`.

Loosely based on
[flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/), this
PR adds a `Flex` enum with the following variants:

- Start
- Center
- End
- SpaceAround
- SpaceBetween

<img width="380" alt="image" src="https://github.com/ratatui-org/ratatui/assets/1813121/b744518c-eae7-4e35-bbc4-fe3c95193cde">

It also adds two more variants, to make this backward compatible and to
make it replace `SegmentSize`:

- StretchLast (default in the `Flex` enum, also behavior matches old
  default `SegmentSize::LastTakesRemainder`)
- Stretch (behavior matches `SegmentSize::EvenDistribution`)

The `Start` variant from above matches `SegmentSize::None`.

This allows `Flex` to be a complete replacement for `SegmentSize`, hence
this PR also deprecates the `segment_size` constructor on `Layout`.
`SegmentSize` is still used in `Table` but under the hood `segment_size`
maps to `Flex` with all tests passing unchanged.

I also put together a simple example for `Flex` layouts so that I could
test it visually, shared below:

https://github.com/ratatui-org/ratatui/assets/1813121/c8716c59-493f-4631-add5-feecf4bd4e06
2024-01-13 01:56:27 -08:00
Dheepak Krishnamurthy
9a3815b66d feat: Add Constraint::Fixed and Constraint::Proportional (#783) 2024-01-12 21:11:15 -05:00
Dheepak Krishnamurthy
425a65140b feat: Add comprehensive tests for Length interacting with other constraints (#802) 2024-01-12 20:40:43 -05:00
multisn8
fe06f0c7b0 feat(serde): support TableState, ListState, and ScrollbarState (#723)
TableState, ListState, and ScrollbarState can now be serialized and deserialized
using serde.

```rust
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct AppState {
    list_state: ListState,
    table_state: TableState,
    scrollbar_state: ScrollbarState,
}

let app_state = AppState::default();
let serialized = serde_json::to_string(app_state);

let app_state = serde_json::from_str(serialized);
```
2024-01-12 16:13:35 -08:00
Christian Stefanescu
43b2b57191 docs: fix typo in Table widget description (#797) 2024-01-12 21:11:56 +01:00
Valentin271
50b81c9d4e fix(examples/scrollbar): title wasn't displayed because of background reset (#795) 2024-01-12 17:53:35 +01:00
Dheepak Krishnamurthy
2b4aa46a6a docs: GitHub admonition syntax for examples README.md (#791)
* docs: GitHub admonition syntax for examples README.md

* docs: Add link to stable release
2024-01-11 21:08:54 -05:00
Josh McKinney
e1e85aa7af feat(style): add material design color palette (#786)
The `ratatui::style::palette::material` module contains the Google 2014
Material Design palette.

See https://m2.material.io/design/color/the-color-system.html#tools-for-picking-colors
for more information.

```rust
use ratatui::style::palette::material::BLUE_GRAY;
Line::styled("Hello", BLUE_GRAY.c500);
```
2024-01-11 19:22:57 +01:00
Josh McKinney
bf67850739 feat(style): add tailwind color palette (#787)
The `ratatui::style::palette::tailwind` module contains the default
Tailwind color palette. This is useful for styling components with
colors that match the Tailwind color palette.

See https://tailwindcss.com/docs/customizing-colors for more information
on Tailwind.

```rust
use ratatui::style::palette::tailwind::SLATE;
Line::styled("Hello", SLATE.c500);
```
2024-01-11 10:07:53 -08:00
Josh McKinney
1561d64c80 feat(layout): add Rect -> Size conversion methods (#789)
- add Size::new() constructor
- add Rect::as_size()
- impl From<Rect> for Size
- document and add tests for Size
2024-01-11 17:39:53 +01:00
Josh McKinney
ffd5fc79fc feat(color): add Color::from_u32 constructor (#785)
Convert a u32 in the format 0x00RRGGBB to a Color.

```rust
let white = Color::from_u32(0x00FFFFFF);
let black = Color::from_u32(0x00000000);
```
2024-01-11 04:23:16 -08:00
Josh McKinney
34648941d4 docs(examples): add warning about examples matching the main branch (#778) 2024-01-11 08:44:23 +01:00
Eric Lunderberg
c69ca47922 feat(table)!: Collect iterator of Row into Table (#774)
Any iterator whose item is convertible into `Row` can now be
collected into a `Table`.

Where previously, `Table::new` accepted `IntoIterator<Item = Row>`, it
now accepts `IntoIterator<Item: Into<Row>>`.

BREAKING CHANGE:
The compiler can no longer infer the element type of the container
passed to `Table::new()`.  For example, `Table::new(vec![], widths)`
will no longer compile, as the type of `vec![]` can no longer be
inferred.
2024-01-10 17:32:58 -08:00
Eric Lunderberg
f29c73fb1c feat(tabs): accept Iterators of Line in constructors (#776)
Any iterator whose item is convertible into `Line` can now be
collected into `Tabs`.

In addition, where previously `Tabs::new` required a `Vec`, it can now
accept any object that implements `IntoIterator` with an item type
implementing `Into<Line>`.

BREAKING CHANGE:

Calls to `Tabs::new()` whose argument is collected from an iterator
will no longer compile.  For example,
`Tabs::new(["a","b"].into_iter().collect())` will no longer compile,
because the return type of `.collect()` can no longer be inferred to
be a `Vec<_>`.
2024-01-10 17:16:44 -08:00
Dheepak Krishnamurthy
f2eab71ccf fix: broken tests in table.rs (#784)
* fix: broken tests in table.rs

* fix: Use default instead of raw
2024-01-10 19:57:38 +01:00
Josh McKinney
6645d2e058 fix(table)!: ensure that default and new() match (#751)
In https://github.com/ratatui-org/ratatui/pull/660 we introduced the
segment_size field to the Table struct. However, we forgot to update
the default() implementation to match the new() implementation. This
meant that the default() implementation picked up SegmentSize::default()
instead of SegmentSize::None.

Additionally the introduction of Table::default() in an earlier PR,
https://github.com/ratatui-org/ratatui/pull/339, was also missing the
default for the column_spacing field (1).

This commit fixes the default() implementation to match the new()
implementation of these two fields by implementing the Default trait
manually.

BREAKING CHANGE: The default() implementation of Table now sets the
column_spacing field to 1 and the segment_size field to
SegmentSize::None. This will affect the rendering of a small amount of
apps.
2024-01-10 17:16:03 +01:00
Josh McKinney
2faa879658 feat(table): accept Text for highlight_symbol (#781)
This allows for multi-line symbols to be used as the highlight symbol.

```rust
let table = Table::new(rows, widths)
    .highlight_symbol(Text::from(vec![
        "".into(),
        " █ ".into(),
        " █ ".into(),
        "".into(),
    ]));
```
2024-01-10 17:11:37 +01:00
Eric Lunderberg
eb79256cee feat(widgets): Collect iterator of ListItem into List (#775)
Any iterator whose item is convertible into `ListItem` can now be
collected into a `List`.

```rust
let list: List = (0..3).map(|i| format!("Item{i}")).collect();
```
2024-01-10 00:06:03 -08:00
Josh McKinney
388aa467f1 docs: update crate, lib and readme links (#771)
Link to the contributing, changelog, and breaking changes docs at the
top of the page instead of just in in the main part of the doc. This
makes it easier to find them.

Rearrange the links to be in a more logical order.

Use link refs for all the links

Fix up the CI link to point to the right workflow
2024-01-08 23:52:47 -08:00
Valentin271
8dd177a051 fix: fix PR write permission to upload unsigned commit comment (#770) 2024-01-08 16:26:15 +01:00
Prisacaru Bogdan-Paul
bbf2f906fb feat(rect.rs): implement Rows and Columns iterators in Rect (#765)
This enables iterating over rows and columns of a Rect. In tern being able to use that with other iterators and simplify looping over cells.
2024-01-08 15:51:19 +01:00
Valentin271
c24216cf30 chore: add comment on PRs with unsigned commits (#768) 2024-01-08 04:30:06 -08:00
Valentin271
5db895dcbf chore(deps): update termion to v3.0 (#767)
See Termion changelog at https://gitlab.redox-os.org/redox-os/termion#200-to-300-guide
2024-01-08 11:38:41 +01:00
Dheepak Krishnamurthy
c50ff08a63 feat: Add frame count (#766) 2024-01-08 03:51:53 -05:00
Valentin271
06141900b4 fix(cd): fix grepping the last release (#762) 2024-01-07 16:14:16 +01:00
Valentin271
fab943b61a chore(contributing): add deprecation notice guideline (#761) 2024-01-07 14:44:51 +01:00
Valentin271
a67815e138 fix(chart): exclude unnamed datasets from legend (#753)
A dataset with no name won't display an empty line anymore in the legend.
If no dataset have name, then no legend is ever displayed.
2024-01-07 13:21:38 +01:00
Valentin271
bd6b91c958 refactor!: make patch_style & reset_style chainable (#754)
Previously, `patch_style` and `reset_style` in `Text`, `Line` and `Span`
 were using a mutable reference to `Self`. To be more consistent with
 the rest of `ratatui`, which is using fluent setters, these now take
 ownership of `Self` and return it.
2024-01-07 12:58:13 +01:00
Josh McKinney
5aba988fac refactor(terminal): extract types to files (#760)
Fields on Frame that were private are now pub(crate).
2024-01-07 12:53:16 +01:00
Eric Lunderberg
e64e194b6b feat(table): Implement FromIterator for widgets::Row (#755)
The `Row::new` constructor accepts a single argument that implements
`IntoIterator`.  This commit adds an implementation of `FromIterator`,
as a thin wrapper around `Row::new`.  This allows `.collect::<Row>()`
to be used at the end of an iterator chain, rather than wrapping the
entire iterator chain in `Row::new`.
2024-01-06 20:23:38 -08:00
Valentin271
bc274e2bd9 refactor(block)!: remove deprecated title_on_bottom (#757)
`Block::title_on_bottom` was deprecated in v0.22. Use `Block::title` and `Title::position` instead.
2024-01-06 14:26:02 -08:00
Josh McKinney
f13fd73d9e feat(layout): add Rect::clamp() method (#749)
* feat(layout): add a Rect::clamp() method

This ensures a rectangle does not end up outside an area. This is useful
when you want to be able to dynamically move a rectangle around, but
keep it constrained to a certain area.

For example, this can be used to implement a draggable window that can
be moved around, but not outside the terminal window.

```rust
let window_area = Rect::new(state.x, state.y, 20, 20).clamp(area);
state.x = rect.x;
state.y = rect.y;
```

* refactor: use rstest to simplify clamp test

* fix: use rstest description instead of string

test layout::rect::tests::clamp::case_01_inside ... ok
test layout::rect::tests::clamp::case_02_up_left ... ok
test layout::rect::tests::clamp::case_04_up_right ... ok
test layout::rect::tests::clamp::case_05_left ... ok
test layout::rect::tests::clamp::case_03_up ... ok
test layout::rect::tests::clamp::case_06_right ... ok
test layout::rect::tests::clamp::case_07_down_left ... ok
test layout::rect::tests::clamp::case_08_down ... ok
test layout::rect::tests::clamp::case_09_down_right ... ok
test layout::rect::tests::clamp::case_10_too_wide ... ok
test layout::rect::tests::clamp::case_11_too_tall ... ok
test layout::rect::tests::clamp::case_12_too_large ... ok

* fix: less ambiguous docs for this / other rect

* fix: move rstest to dev deps
2024-01-05 20:38:30 +01:00
Josh McKinney
fe84141119 docs(layout): document the difference in the split methods (#750)
* docs(layout): document the difference in the split methods

* fix: doc suggestion
2024-01-05 20:19:53 +01:00
Josh McKinney
fb93db0730 docs(examples): simplify docs using new layout methods (#731)
Use the new `Layout::horizontal` and `vertical` constructors and
`Rect::split_array` through all the examples.
2024-01-05 07:45:14 -08:00
Valentin271
23f6938498 feat(block): add Block::bordered (#736)
This avoid creating a block with no borders and then settings Borders::ALL. i.e.

```diff
- Block::default().borders(Borders::ALL);
+ Block::bordered();
```
2024-01-05 12:19:49 +01:00
Josh McKinney
98bcf1c0a5 feat(layout): add Rect::split method (#729)
This method splits a Rect and returns a fixed-size array of the
resulting Rects. This allows the caller to use array destructuring
to get the individual Rects.

```rust
use Constraint::*;
let layout = &Layout::vertical([Length(1), Min(0)]);
let [top, main] = area.split(&layout);
```
2024-01-05 03:13:57 -08:00
Josh McKinney
803a72df27 feat(table): accept Into<Constraint> for widths (#745)
This allows Table constructors to accept any type that implements
Into<Constraint> instead of just AsRef<Constraint>. This is useful when
you want to specify a fixed size for a table columns, but don't want to
explicitly create a Constraint::Length yourself.

```rust
Table::new(rows, [1,2,3])
Table::default().widths([1,2,3])
```
2024-01-04 22:41:48 -08:00
Josh McKinney
0494ee52f1 feat(layout): accept Into<Constraint> for constructors (#744)
This allows Layout constructors to accept any type that implements
Into<Constraint> instead of just AsRef<Constraint>. This is useful when
you want to specify a fixed size for a layout, but don't want to
explicitly create a Constraint::Length yourself.

```rust
Layout::new(Direction::Vertical, [1, 2, 3]);
Layout::horizontal([1, 2, 3]);
Layout::vertical([1, 2, 3]);
Layout::default().constraints([1, 2, 3]);
```
2024-01-04 22:36:37 -08:00
Josh McKinney
6d15b2570f refactor(layout): move the remaining types (#743)
- alignment -> layout/alignment.rs
- corner -> layout/corner.rs
- direction -> layout/direction.rs
- size -> layout/size.rs
2024-01-04 22:35:12 -08:00
Josh McKinney
659460e19c refactor(layout): move SegmentSize to layout/segment_size.rs (#742) 2024-01-04 20:53:57 -08:00
Josh McKinney
ba036cd579 refactor(layout): move Layout to layout/layout.rs (#741) 2024-01-04 20:43:00 -08:00
Josh McKinney
8724aeb9e7 refactor(layout): move Margin to margin.rs (#740) 2024-01-04 20:34:42 -08:00
Josh McKinney
da6c299804 refactor: extract layout::Constraint to file (#739) 2024-01-04 20:13:11 -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
Orhun Parmaksız
3f781cad0a chore(release): prepare for 0.23.0 (#444) 2023-08-28 11:46:03 +00:00
Hichem
fc727df7d2 refactor(barchart): reduce some calculations (#430)
Calculating the label_offset is unnecessary, if we just render the
group label after rendering the bars. We can just reuse bar_y.

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>
2023-08-27 21:10:49 +00:00
Orhun Parmaksız
47fe4ad69f docs(project): make the project description cooler (#441)
* docs(project): make the project description cooler

* docs(lib): simplify description
2023-08-27 20:58:54 +00:00
Orhun Parmaksız
7a70602ec6 docs(examples): fix the instructions for generating demo GIF (#442) 2023-08-27 20:50:33 +00:00
Josh McKinney
14eb6b6979 test(tabs): add unit tests (#439) 2023-08-27 09:38:16 +00:00
Orhun Parmaksız
6009844e25 chore(changelog): ignore alpha tags (#440) 2023-08-27 09:01:17 +00:00
Orhun Parmaksız
8b36683571 docs(lib): extract feature documentation from Cargo.toml (#438)
* docs(lib): extract feature documentation from Cargo.toml

* chore(deps): make `document-features` optional dependency

* docs(lib): document the serde feature from features section
2023-08-27 09:00:35 +00:00
Josh McKinney
e9bd736b1a test(clear): test Clear rendering (#432) 2023-08-26 21:32:23 +00:00
Josh McKinney
a890f2ac00 test(block): test all block methods (#431) 2023-08-26 21:32:01 +00:00
Josh McKinney
b35f19ec44 test(test_backend): add tests for TestBackend coverage (#434)
These are mostly to catch any future bugs introduced in the test backend
2023-08-26 07:25:16 +00:00
Josh McKinney
ad3413eeec test(canvas): add unit tests for line (#437)
Also add constructor to simplify creating lines
2023-08-26 04:38:51 +00:00
Josh McKinney
f0716edbcf test(map): add unit tests (#436) 2023-08-26 01:09:55 +00:00
Josh McKinney
fc9f637fb0 test(text): add unit tests (#435) 2023-08-26 01:08:12 +00:00
Josh McKinney
292a11d81e test(styled_grapheme): test StyledGrapheme methods (#433) 2023-08-26 01:02:30 +00:00
Josh McKinney
ad4d6e7dec test(canvas): add tests for rectangle (#429) 2023-08-25 11:50:10 +00:00
Benjamin Grosse
e4bcf78afa feat(cell): add voluntary skipping capability for sixel (#215)
> Sixel is a bitmap graphics format supported by terminals.
> "Sixel mode" is entered by sending the sequence ESC+Pq.
> The "String Terminator" sequence ESC+\ exits the mode.

The graphics are then rendered with the top left positioned at the
cursor position.

It is actually possible to render sixels in ratatui with just
`buf.get_mut(x, y).set_symbol("^[Pq ... ^[\")`. But any buffer covering
the "image area" will overwrite the graphics. This is most likely the same
buffer, even though it consists of empty characters `' '`, except for
the top-left character that starts the sequence.

Thus, either the buffer or cells must be specialized to avoid drawing
over the graphics. This patch specializes the `Cell` with a
`set_skip(bool)` method, based on James' patch:
https://github.com/TurtleTheSeaHobo/tui-rs/tree/sixel-support
I unsuccessfully tried specializing the `Buffer`, but as far as I can tell
buffers get merged all the way "up" and thus skipping must be set on the
Cells. Otherwise some kind of "skipping area" state would be required,
which I think is too complicated.

Having access to the buffer now it is possible to skipp all cells but the
first one which can then `set_symbol(sixel)`. It is up to the user to
deal with the graphics size and buffer area size. It is possible to get
the terminal's font size in pixels with a syscall.

An image widget for ratatui that uses this `skip` flag is available at
https://github.com/benjajaja/ratatu-image.

Co-authored-by: James <james@rectangle.pizza>
2023-08-25 09:20:36 +00:00
Josh McKinney
d0ee04a69f docs(span): update docs and tests for Span (#427) 2023-08-25 09:08:05 +00:00
Josh McKinney
6d6eceeb88 docs(paragraph): add more docs (#428) 2023-08-25 04:43:19 +00:00
Hichem
0dca6a689a feat(barchart): Add direction attribute. (horizontal bars support) (#325)
* feat(barchart): Add direction attribute

Enable rendring the bars horizontally. In some cases this allow us to
make more efficient use of the available space.

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>

* feat(barchart)!: render the group labels depending on the alignment

This is a breaking change, since the alignment by default is set to
Left and the group labels are always rendered in the center.

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>

---------

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>
2023-08-24 22:26:15 +00:00
Josh McKinney
a937500ae4 chore(changelog): show full commit message (#423)
This allows someone reading the changelog to search for information
about breaking changes or implementation of new functionality.

- refactored the commit template part to a macro instead of repeating it
- added a link to the commit and to the release
- updated the current changelog for the alpha and unreleased changes
- Automatically changed the existing * lists to - lists
2023-08-24 07:59:59 +00:00
Geert Stappers
80fd77e476 Matrix URL (#342)
* docs(readme): add links to matrix bridge

* Update README.md

Co-authored-by: Dheepak Krishnamurthy <me@kdheepak.com>

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
Co-authored-by: Dheepak Krishnamurthy <me@kdheepak.com>
2023-08-23 16:44:39 +00:00
Josh McKinney
98155dce25 chore(traits): add Display and FromStr traits (#425)
Use strum for most of these, with a couple of manual implementations,
and related tests
2023-08-23 04:21:13 +00:00
hasezoey
1ba2246d95 Document all features in one place (#391)
* chore(Cargo.toml): change "time" to "dep:time"

* docs(lib): document optional and default features
2023-08-23 03:54:42 +00:00
a-kenji
57ea871753 feat: expand serde attributes for TestBuffer (#389) 2023-08-22 12:51:06 +00:00
Dheepak Krishnamurthy
61533712be feat: Add weak constraints to make rects closer to each other in size (#395)
Also make `Max` and `Min` constraints MEDIUM strength for higher priority over equal chunks
2023-08-20 17:40:04 +00:00
Josh McKinney
dc552116cf fix(table): fix unit tests broken due to rounding (#419)
The merge of the table unit tests after the rounding layout fix was not
rebased correctly, this addresses the broken tests, makes them more
concise while adding comments to help clarify that the rounding behavior
is working as expected.
2023-08-20 00:15:20 +00:00
hasezoey
ab5e616635 style(paragraph): add documentation for "scroll"'s "offset" (#355)
* style(paragraph): add documentation for "scroll"'s "offset"

* style(paragraph): add more text to the scroll doc-comment
2023-08-19 21:17:46 +00:00
Orhun Parmaksız
b6b2da5eb7 fix(release): fix the last tag retrieval for alpha releases (#416) 2023-08-19 13:14:17 +00:00
Orhun Parmaksız
89ef0e29f5 chore(ci): update the name of the CI workflow (#417) 2023-08-19 13:14:01 +00:00
hasezoey
4cd843eda9 test(table): add test for consistent table-column-width (#404) 2023-08-19 12:47:23 +00:00
Dheepak Krishnamurthy
d2429bc3e4 chore: Create rust-toolchain.toml (#415) 2023-08-19 03:51:53 +00:00
Dheepak Krishnamurthy
b090101b23 feat: Simplify split function (#411) 2023-08-18 14:43:03 +00:00
Josh McKinney
56455e0fee fix(layout): don't leave gaps between chunks (#408)
Previously the layout used the floor of the calculated start and width
as the value to use for the split Rects. This resulted in gaps between
the split rects.

This change modifies the layout to round to the nearest column instead
of taking the floor of the start and width. This results in the start
and end of each rect being rounded the same way and being strictly
adjacent without gaps.

Because there is a required constraint that ensures that the last end is
equal to the area end, there is no longer the need to fixup the last
item width when the fill (as e.g. width = x.99 now rounds to x+1 not x).

The colors example has been updated to use Ratio(1, 8) instead of
Percentage(13), as this now renders without gaps for all possible sizes,
whereas previously it would have left odd gaps between columns.
2023-08-18 10:23:13 +00:00
Josh McKinney
f4ed3b7584 fix(layout): ensure left <= right (#410)
The recent refactor missed the positive width constraint
2023-08-17 20:15:25 +00:00
hasezoey
c86924b925 Span tests (#406)
* test(span): add some tests for "Span"

* feat(span): replace all "From<*>" with one "From<Into<Cow>>>"
2023-08-17 08:32:26 +00:00
Josh McKinney
de25de0a95 refactor(layout): simplify and doc split() (#405)
* test(layout): add tests for split()

* refactor(layout): simplify and doc split()

This is mainly a reduction in density of the code with a goal of
improving mainatainability so that the algorithm is clear.
2023-08-17 07:44:33 +00:00
hasezoey
ea48af1c9a chore(codecov): fix yaml syntax (#407)
a yaml file cannot contain tabs outside of strings
2023-08-16 10:21:21 +00:00
Josh McKinney
418ed20479 docs(layout): add doc comments (#403) 2023-08-16 00:10:28 +00:00
hasezoey
519509945b refactor(layout): simplify split() function (#396)
Removes some unnecessary code and makes the function more readable.
Instead of creating a temporary result and mutating it, we just create
the result directly from the list of changes.
2023-08-14 23:17:21 +00:00
Josh McKinney
8c55158822 chore: use vhs to create demo.gif (#390)
The bug that prevented braille rendering is fixed, so switch to VHS for
rendering the demo gif

![Demo of Ratatui](https://vhs.charm.sh/vhs-tF0QbuPbtHgUeG0sTVgFr.gif)
2023-08-13 16:21:00 +00:00
Jan Keith Darunday
7748720963 feat(table): add support for line alignment in the table widget (#392)
* feat(table): enforce line alignment in table render

* test(table): add table alignment render test
2023-08-13 08:32:27 +00:00
hasezoey
4d70169bef feat(list): add option to always allocate the "selection" column width (#394)
* feat(list): add option to always allocate the "selection" column width

Before this option was available, selecting a item in a list when nothing was selected
previously made the row layout change (the same applies to unselecting) by adding the width
of the "highlight symbol" in the front of the list, this option allows to configure this
behavior.

* style: change "highlight_spacing" doc comment to use inline code-block for reference
2023-08-13 08:24:51 +00:00
Josh McKinney
10dbd6f207 docs(examples): show layout constraints (#393)
Shows the way that layout constraints interact visually

![example](https://vhs.charm.sh/vhs-1ZNoNLNlLtkJXpgg9nCV5e.gif)
2023-08-13 07:38:43 +00:00
Orhun Parmaksız
778c320008 fix(release): set the correct permissions for creating alpha releases (#400) 2023-08-12 19:48:13 +00:00
Orhun Parmaksız
268bbed17e chore(make): add task descriptions to Makefile.toml (#398) 2023-08-12 19:48:00 +00:00
hasezoey
f63ac72305 feat(widgets::table): add option to always allocate the "selection" constraint (#375)
* feat(table): add option to configure selection layout changes

Before this option was available, selecting a row in the table when no row was selected
previously made the tables layout change (the same applies to unselecting) by adding the width
of the "highlight symbol" in the front of the first column, this option allows to configure this
behavior.

* refactor(table): refactor "get_columns_widths" to return (x, width)

and "render" to make use of that

* refactor(table): refactor "get_columns_widths" to take in a selection_width instead of a boolean

also refactor "render" to make use of this change

* fix(table): rename "highlight_set_selection_space" to "highlight_spacing"

* style(table): apply doc-comment suggestions from code review

Co-authored-by: Dheepak Krishnamurthy <me@kdheepak.com>

---------

Co-authored-by: Dheepak Krishnamurthy <me@kdheepak.com>
2023-08-11 14:15:46 +00:00
Valentin271
3293c6b80b test(sparkline): added benchmark (#384)
Added benchmark for the `sparkline` widget testing a basic render with different amout of data
2023-08-11 02:17:32 +00:00
Valentin271
149d48919d perf(bench): Used iter_batched to clone widgets in setup function (#383)
Replaced `Bencher::iter` by `Bencher::iter_batched` to clone the widget in the setup function instead of in the benchmark timing.
2023-08-11 02:17:14 +00:00
tieway59
8c4a2e0fbf chore: implement Hash common traits (#381)
Reorder the derive fields to be more consistent:

    Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash

Hash trait won't be impl in this PR due to rust std design.
If we need hash trait for f64 related structs in the future,
we should consider wrap f64 into a new type.

see: https://github.com/ratatui-org/ratatui/issues/307
2023-08-11 02:16:48 +00:00
Valentin271
664fb4cffd test(list): Added benchmarks (#377)
Added benchmarks for the list widget (render and render half scrolled)
2023-08-11 02:11:41 +00:00
Josh McKinney
6ad4bd4cf2 docs(examples): Add color and modifiers examples (#345)
The intent of these examples is to show the available colors and
modifiers.

- added impl Display for Color

![colors](https://vhs.charm.sh/vhs-2ZCqYbTbXAaASncUeWkt1z.gif)
![modifiers](https://vhs.charm.sh/vhs-2ovGBz5l3tfRGdZ7FCw0am.gif)
2023-08-11 01:36:12 +00:00
a-kenji
37fa6abe9d build(deps): upgrade crossterm to 0.27 (#380) 2023-08-07 13:30:11 +00:00
a-kenji
8b28672131 chore(docs): add doc comment bump to release documentation (#382) 2023-08-07 13:30:03 +00:00
a-kenji
de9f52ff2c ci(coverage): exclude examples directory from coverage (#373) 2023-08-07 12:34:04 +00:00
hasezoey
c8ddc164c7 docs(layout::Constraint): add doc-comments for all variants (#371)
fixes #354
2023-08-05 22:40:25 +00:00
Valentin271
e18393dbc6 test(block): add benchmarks (#368)
Added benchmarks to the block widget to uncover eventual performance issues
2023-08-05 14:47:06 +00:00
Orhun Parmaksız
aad164a531 feat(release): add automated nightly releases (#359)
* feat(release): add automated nightly releases

* refactor(release): rename the alpha workflow

* refactor(release): simplify the release calculation
2023-08-05 14:41:07 +00:00
Valentin271
3a37d2f6ed docs(readme): use the correct version for MSRV (#369) 2023-08-05 10:20:30 +00:00
a-kenji
8cd3205d70 chore(toolchain)!: bump msrv to 1.67 (#361)
* chore(toolchain)!: bump msrv to 1.67

BREAKING_CHANGE: The msrv is now `1.67`

* docs(readme): update the MSRV notice

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2023-08-04 23:23:22 +00:00
Josh McKinney
e82521ea79 docs(examples): regen block.gif in readme (#365) 2023-08-04 10:12:13 +00:00
Josh McKinney
9191ad60fd ci: don't fail fast (#364)
Run all the tests rather than canceling when one test fails. This allows
us to see all the failures, rather than just the first one if there are
multiple. Specifically this is useful when we have an issue in one
toolchain or backend.
2023-08-04 09:56:05 +00:00
Valentin271
49a82e062f fix(block): Fixed title_style not rendered (#349) (#363)
Fixes #349
2023-08-04 09:47:55 +00:00
tieway59
181706c564 chore: implement Eq & PartialEq common traits (#357)
Reorder the derive fields to be more consistent:

    Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash

see: https://github.com/ratatui-org/ratatui/issues/307
2023-08-04 08:23:26 +00:00
Josh McKinney
554805d6cb docs(examples): Update block example (#351)
![Block example](https://vhs.charm.sh/vhs-5X6hpReuDBKjD6hLxmDQ6F.gif)
2023-08-04 07:46:37 +00:00
a-kenji
1727fa5120 feat(scrollbar)!: add optional track symbol (#360)
The track symbol is now optional, simplifying composition with other
widgets.

BREAKING_CHANGE: The `track_symbol` needs to be set in the following way
now:

```
let scrollbar = Scrollbar::default().track_symbol(Some("-"));
```
2023-08-03 15:42:54 +00:00
tieway59
440f62ff54 Chore: implement Clone & Copy common traits (#350)
Implement `Clone & Copy` common traits for most structs in src.

Only implement `Copy` for structs that are simple and trivial to copy.

Reorder the derive fields to be more consistent:

    Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash

see: https://github.com/ratatui-org/ratatui/issues/307
2023-07-28 11:04:29 +00:00
Josh McKinney
6f659cfb07 ci: add coverage token (#352) 2023-07-28 06:57:53 +00:00
tieway59
bf4944683d Chore: implement Debug & Default common traits (#339)
Implement `Debug & Default` common traits for most structs in src.

Reorder the derive fields to be more consistent:

    Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash

see: https://github.com/ratatui-org/ratatui/issues/307
2023-07-27 01:40:07 +00:00
Josh McKinney
7539f775fe fix(scrollbar)!: move symbols to symbols module (#330)
The symbols and sets are moved from `widgets::scrollbar` to
`symbols::scrollbar`. This makes it consistent with the other symbol
sets and allows us to make the scrollbar module private rather than
re-exporting it.

BREAKING CHANGE: The symbols are now in the `symbols` module. To update
your code, add an import for `ratatui::symbols::scrollbar::*` (or the
specific symbols you need). The scrollbar module is no longer public. To
update your code, remove any `widgets::scrollbar` imports and replace it
with `ratatui::widgets::Scrollbar`.
2023-07-26 11:33:39 +00:00
nathan
8db9fb4aeb fix(cargo): adjust minimum paste version (#348)
ratatui is using features that are currently only available in paste 1.0.2; specifying the minimum version to be 1.0 will consequently cause a compilation error if cargo is only able to use a version less than 1.0.2.
2023-07-26 07:05:12 +00:00
Florian Meißner
d05ab6fb70 fix(readme): fix typo in readme (#344)
Co-authored-by: josh rotenberg <joshrotenberg@users.noreply.github.com>
2023-07-25 21:47:28 +00:00
Josh McKinney
2920e045ba docs(readme): fix widget docs links (#346)
Add scrollbar, clear. Fix Block link. Sort
2023-07-25 21:44:50 +00:00
Josh McKinney
add578a7d6 docs(examples): Add examples readme with gifs (#303)
This commit adds a readme to the examples directory with gifs of each
example. This should make it easier to see what each example does
without having to run it.

I modified the examples to fit better in the gifs. Mostly this was just
removing the margins, but for the block example I cleaned up the code a
bit to make it more readable and changed it so the background bug is not
triggered.

For the table example, the combination of Min, Length, and Percent
constraints was causing the table to panic when the terminal was too
small. I changed the example to use the Max constraint instead of the
Length constraint.

The layout example now shows information about how the layout is
constrained on each block (which is now a paragraph with a block).
2023-07-24 19:05:37 +00:00
Orhun Parmaksız
60a4131384 chore(github): add kdheepak as a maintainer (#343) 2023-07-22 22:12:08 +00:00
Orhun Parmaksız
964190a859 chore(github): rename tui-rs-revival references to ratatui-org (#340) 2023-07-22 10:34:11 +00:00
josh rotenberg
b9290b35d1 fix(readme): fix incorrect template link (#338) 2023-07-20 21:36:15 +00:00
josh rotenberg
daf5890152 fix(example): Fix typo (#337)
the existential feels
2023-07-20 20:00:42 +00:00
josh rotenberg
7e37a96678 fix(readme): fix typo in readme (#336) 2023-07-20 04:39:46 +00:00
josh rotenberg
bcb7417785 update version in README.md (#335) 2023-07-20 04:29:08 +00:00
Hichem
9c956733f7 fix(barchart): empty groups causes panic (#333)
This unlikely to happen, since nobody wants to add an empty group.
Even we fix the panic, things will not render correctly.
So it is better to just not add them to the BarChart.

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>
2023-07-19 09:37:59 +00:00
Markus
13fb11a62c fix: Correct minor typos in documentation (#331) 2023-07-18 10:06:59 +00:00
EdJoPaTo
0fb1ed85c6 build: forbid unsafe code (#332)
This indicates good (high level) code and is used by tools like cargo-geiger.
2023-07-18 10:03:00 +00:00
Josh McKinney
e2cb11cc30 build(examples): fix cargo make run-examples (#327)
Enables the all-widgets feature so that the calendar example runs correctly
2023-07-17 15:59:56 +00:00
a-kenji
c3f87f245a docs: improve scrollbar doc comment (#329) 2023-07-17 12:05:28 +00:00
Orhun Parmaksız
df90982632 chore(release): prepare for 0.22.0 (#326) 2023-07-17 10:41:45 +00:00
Josh McKinney
bb061fdab6 ci: parallelize CI jobs (#318)
* ci: parallelize CI jobs

- remove the dependency on the lint job from all other jobs
- implement workflow concurrency
- reorder the workflow so that the lint, clippy and coverage jobs are
  scheduled before the test jobs
- run jobs which run for each backend in parallel by calling e.g.
  cargo make test-termion, instead of cargo make test
- add a coverage task to the makefile
- change "cargo-make check" to check all features valid for OS in
  parallel
- run clippy only on the ubuntu-latest runner and check all features
  valid in parallel
- tidy up the workflow file

* ci: simplify Makefile OS detection

Use platform overrides to significantly simplify the Makefile logic
See https://github.com/sagiegurari/cargo-make\#platform-override

* fix(termwiz): skip doc test that requires stdout
2023-07-17 10:31:31 +00:00
Orhun Parmaksız
1ff85535c8 fix(title): remove default alignment and position (#323)
* fix(title): remove default alignment and position

* test(block): add test cases for alignment

* test(block): extend the unit tests for block title alignment
2023-07-17 10:27:58 +00:00
Josh McKinney
33f3212cbf fix: rust-tui-template became a revival project (#320)
Changed the URL https://github.com/orhun/rust-tui-template
into https://github.com/rust-tui-revival/rust-tui-template

Co-authored-by: Geert Stappers <stappers@stappers.it>
2023-07-17 06:31:26 +00:00
Josh McKinney
fb6d4b2f51 refactor(text): simplify reflow implementation (#290)
* refactor(text): split text::* into separate files

* feat(text): expose graphemes on Line

- add `Line::styled()`
- add `Line::styled_graphemes()`
- add `StyledGrapheme::new()`

---------

Co-authored-by: Eyesonjune18 <lowellashton@gmail.com>
2023-07-17 06:27:45 +00:00
Josh McKinney
446efae185 fix(prelude): remove widgets module from prelude (#317)
This helps to keep the prelude small and less likely to conflict with
other crates.

- remove widgets module from prelude as the entire module can be just as
  easily imported with `use ratatui::widgets::*;`
- move prelude module into its own file
- update examples to import widgets module instead of just prelude
- added several modules to prelude to make it possible to qualify
  imports that collide with other types that have similar names
2023-07-16 09:11:59 +00:00
Mano Ségransan
b347201b9f feat(style): Enable setting the underline color for crossterm (#308) (#310)
This commit adds the underline_color() function to the Style and Cell
structs. This enables setting the underline color of text on the
crossterm backend. This is a no-op for the termion and termwiz backends
as they do not support this feature.
2023-07-15 09:57:15 +00:00
Josh McKinney
9f1f59a51c feat(stylize): allow all widgets to be styled (#289)
* feat(stylize): allow all widgets to be styled

- Add styled impl to:
  - Barchart
  - Chart (including Axis and Dataset),
  - Guage and LineGuage
  - List and ListItem
  - Sparkline
  - Table, Row, and Cell
  - Tabs
  - Style
- Allow modifiers to be removed (e.g. .not_italic())
- Allow .bg() to recieve Into<Color>
- Made shorthand methods consistent with modifier names (e.g. dim() not
  dimmed() and underlined() not underline())
- Simplify integration tests
- Add doc comments
- Simplified stylize macros with https://crates.io/crates/paste

* build: run clippy before tests

Runny clippy first means that we fail fast when there is an issue that
can easily be fixed rather than having to wait 30-40s for the failure
2023-07-14 08:37:30 +00:00
Josh McKinney
6f6c355c5c chore(tests): add coverage job to bacon (#312)
- Add two jobs to bacon.toml (one for unit tests, one for all tests)
- Remove "run" job as it doesn't work well with bacon due to no stdin
- Document coverage tooling in CONTRIBUTING.md
2023-07-14 08:37:00 +00:00
Hichem
60150f6236 feat(barchart): set custom text value in the bar (#309)
for now the value is converted to a string and then printed. in many
cases the values are too wide or double values. so it make sense
to set a custom value text instead of the default behavior.

this patch suggests to add a method
"fn text_value(mut self, text_value: String)"
to the Bar, which allows to override the value printed in the bar

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>
2023-07-14 04:38:54 +00:00
Josh McKinney
2889c7d084 fix(lint): suspicious_double_ref_op is new in 1.71 (#311)
Fixed tests and completed coverage for `Masked` type.
2023-07-14 02:18:07 +00:00
Florian
57678a5fe8 feat(examples): user_input example cursor movement (#302)
The user_input example now responds to left/right and allows the
character at the cursor position to be deleted / inserted.

Co-authored-by: Leon Sautour <leon1.sautour@epitech.eu>
2023-07-13 10:41:10 +00:00
Hichem
ae8ed8867d feat(barchart): enable barchart groups (#288)
* feat(barchart): allow to add a group of bars

Example: to show the revenue of different companies:
┌────────────────────────┐
│             ████       │
│             ████       │
│      ████   ████       │
│ ▄▄▄▄ ████   ████ ████  │
│ ████ ████   ████ ████  │
│ ████ ████   ████ ████  │
│ █50█ █60█   █90█ █55█  │
│    Mars       April    │
└────────────────────────┘
new structs are introduced: Group and Bar.
the data function is modified to accept "impl Into<Group<'a>>".

a new function "group_gap" is introduced to set the gap between each group

unit test changed to allow the label to be in the center

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>

* feat(barchart)!: center labels by default

The bar labels are currently printed string from the left side of
bar. This commit centers the labels under the bar.

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>

---------

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>
2023-07-13 10:27:08 +00:00
Josh McKinney
e66d5cdee0 docs(color): parse more color formats and add docs (#306) 2023-07-12 13:49:28 +00:00
Josh McKinney
804115ac6f feat(prelude): add a prelude (#304)
This allows users of the library to easily use ratatui without a huge amount of imports
2023-07-10 22:59:01 +00:00
Josh McKinney
a1813af297 test(barchart): add unit tests (#301) 2023-07-09 01:17:34 +00:00
Orhun Parmaksız
085fde7d4a chore(github): add EditorConfig config (#300) 2023-07-08 20:21:24 +00:00
Orhun Parmaksız
860a40c13a style(readme): update the style of badges in README.md (#299) 2023-07-08 20:19:56 +00:00
Josh McKinney
0833c9018b docs: improve CONTRIBUTING.md (#277) 2023-07-08 19:02:22 +00:00
a-kenji
f7c4b44962 feat(style): allow Modifiers add/remove in const (#287)
Allows Modifiers to be added or removed from `Style` in a const context.
This can be used in the following way:

```
const DEFAULT_MODIFIER: Modifier = Modifier::BOLD.union(Modifier::ITALIC);
const DEFAULT_STYLE: Style = Style::new()
.fg(Color::Red).bg(Color::Black).add_modifier(DEFAULT_MODIFIER);
```
2023-07-08 10:12:48 +00:00
Josh McKinney
56e44a0efa chore(license): add Ratatui developers to license (#297) 2023-07-06 12:28:48 +00:00
Josh McKinney
ad288f5168 chore(features): enable building with all-features (#286)
Before this change, it wasn't possible to build all features and all
targets at the same time, which prevents rust-analyzer from working
for the whole project.

Adds a bacon.toml file to the project, which is used by bacon
https://dystroy.org/bacon/

Configures docs.rs to show the feature flags that are necessary to
make modules / types / functions available.
2023-07-04 03:58:25 +00:00
Josh McKinney
c5d387cb53 style: fix formatting (#292)
There are a couple of small formatting changes in the current nightly
2023-07-02 18:47:02 +00:00
Samy Rahmani
2f4413be6e feat: stylization shorthands (#283) 2023-07-01 09:14:16 +00:00
Josh McKinney
83d3ec73e7 fix(clippy): ununsed_mut lint for layout (#285)
This lint is slightly more agressive in +nightly than it is in stable.
2023-06-30 22:01:58 +00:00
Josh McKinney
cf8eda04a1 test(paragraph): simplify paragraph benchmarks (#282)
Reduce benchmarks from 60 calls to 18. Now 3 different line counts
(64, 2048, 65535) * 6 different tests (new, render, scroll half / full,
wrap, wrap and scroll)
2023-06-29 22:12:02 +00:00
SLASHLogin
6bdb97c55c ci(makefile): split CI jobs (#278)
- Split CI into build, clippy and test.
- Run format on nightly only due to the settings being unstable
2023-06-26 07:31:30 +00:00
a-kenji
7a6c3d9db1 feat(misc): make builder fn const (#275) (#275)
This allows the following types to be used in a constant context:
- `Layout`
- `Rect`
- `Style`
- `BorderType`
- `Padding`
- `Block`

Also adds several missing `new()` functions to the above types.

Blocks can now be used in the following way:
```
const DEFAULT_BLOCK: Block = Block::new()
    .title_style(Style::new())
    .title_alignment(Alignment::Left)
    .title_position(Position::Top)
    .borders(Borders::ALL)
    .border_style(Style::new())
    .style(Style::reset())
    .padding(Padding::uniform(1));

```

Layouts can now be used in the following way:
``
const DEFAULT_LAYOUT: Layout = Layout::new()
    .direction(Direction::Horizontal)
    .margin(1)
    .expand_to_fill(false);
```

Rects can now be used in the following way:
```
const RECT: Rect = Rect {
    x: 0,
    y: 0,
    width: 10,
    height: 10,
};
```
2023-06-26 00:09:51 +00:00
a-kenji
bfcc5504bb style(widget): inline format arguments (#279) 2023-06-25 13:19:21 +00:00
SLASHLogin
669a4d5652 build: add git pre-push hooks using cargo-husky (#274)
Fixes https://github.com/tui-rs-revival/ratatui/issues/214
- add cargo-husky to dev-deps
- create hook
- update `CONTRIBUTING.md`
- ensure that the hook is not installed in CI
2023-06-24 06:03:31 +00:00
SLASHLogin
b808305507 docs: fix scrollbar ascii illustrations and calendar doc paths (#272)
* docs(src\widgets\scrollbar.rs): wrap scrollbar's visualisation in text block

'cargo doc' and 'rust-analyzer' removes many whitespaces thus making those parts render improperly

* docs(src/widgets/calendar.rs): fix `no item named ...` for calendar.rs

* style(src/widgets/block.rs): format `block.rs`
2023-06-21 11:57:50 +00:00
Samy Rahmani
a04b190251 feat(block): support for having more than one title (#232) 2023-06-19 08:24:36 +00:00
Leon Sautour
e869869462 ci: add feat-wrapping on push and on pull request ci triggers (#267) 2023-06-19 08:18:27 +00:00
a-kenji
20c0051026 docs(lib): add tui-term a pseudoterminal library (#268) 2023-06-19 08:15:52 +00:00
Orhun Parmaksız
284b0b8de0 chore(github): simplify the CODEOWNERS file (#271) 2023-06-19 00:41:18 +00:00
Dheepak Krishnamurthy
130bdf8337 feat: add scrollbar widget (#228)
Represents a scrollbar widget that renders a track, thumb and arrows
either horizontally or vertically. State is kept in ScrollbarState, and
passed as a parameter to the render function.
2023-06-17 19:25:43 +00:00
Orhun Parmaksız
8b7b7881f5 chore(github): add pull request template (#269) 2023-06-17 17:36:34 +00:00
Ende
28a8435a52 fix(layout): cap Contstraint::apply to 100% length (#264)
This function is only currently used in by the chart widget for
constraining the width and height of the legend.
2023-06-17 10:44:13 +00:00
Josh McKinney
dca9871744 fix: revert removal of WTFPL from deny.toml (#266)
This is actually used for terminfo (transitively from termwiz
2023-06-17 08:54:25 +00:00
Josh McKinney
6c2fbbf275 test: add benchmarks for paragraph (#262)
To run the benchmarks:

    cargo bench

And then open the generated `target/criterion/report/index.html` in a
browser.

- add the BSD 2 clause and ISC licenses to the `cargo deny` allowed
licenses list (as a transitive dependency of the `fakeit` crate).
- remove the WTFPL license from the `cargo deny` allowed licenses list
as it is unused and causes a warning when running the check.
2023-06-16 13:09:41 +00:00
Chris Morris
43bac80e4d fix(examples): Correct progress label in gague example (#263) 2023-06-16 09:17:04 +00:00
Orhun Parmaksız
0bf6af17e7 refactor(ci): simplify cargo-make installation (#240)
* refactor(ci): simplify cargo-make installation

* chore(ci): use the latest version of cargo-make

* refactor(ci): remove unused triple values

* chore(ci): list all steps before ci

* fix(ci): checkout the repository

* refactor(ci): remove unnecessary os variables

* refactor(ci): use dtolnay/rust-toolchain action
2023-06-12 23:25:18 +00:00
Josh McKinney
f7af8a3863 style: reformat imports (#219)
Order imports by std, external, crate and group them by crate
2023-06-12 05:07:15 +00:00
Orhun Parmaksız
492af7a92d chore(ci): bump cargo-make version (#239) 2023-06-11 21:01:21 +00:00
Orhun Parmaksız
de4f2b9990 chore(demo): update demo gif (#234) 2023-06-11 20:33:25 +00:00
Orhun Parmaksız
4a2ff204ec chore(ci): enable merge queue for builds (#235)
* chore(ci): enable merge queue for builds

https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue

* style(ci): format the ci workflow
2023-06-11 20:28:17 +00:00
Orhun Parmaksız
26dbb29b3d style(manifest): apply formatting to Cargo.toml (#237) 2023-06-11 20:27:35 +00:00
Orhun Parmaksız
231aae2920 style(config): apply formatting to config files (#238) 2023-06-11 20:26:45 +00:00
Orhun Parmaksız
4cc7380a88 chore(github): fix the syntax in CODEOWNERS file (#236) 2023-06-11 20:25:14 +00:00
Josh McKinney
593fd29d00 chore(demo): update demo gif with a fixed unicode gauge (#227)
* fix(gauge): render gauge with unicode correctly

Gauge now correctly renders a block rather than a space when in unicode mode.

* docs: update demo.gif

- remove existing gif
- upload using VHS (https://github.com/charmbracelet/vhs)
- add instructions to RELEASE.md
- link new gif in README
2023-06-11 16:23:23 +02:00
Philipp Mildenberger
f84d97b17b feat(terminal): expose 'swap_buffers' method 2023-06-10 22:39:55 -07:00
Josh McKinney
ef4d743af3 fix(typos): configure typos linter (#233)
- Adds a new typos.toml
- Prevents ratatui from being marked as a typo of ratatouille
- Changes paragraph tests so that the truncated word is a valid word
2023-06-10 16:31:36 -07:00
Josh McKinney
9ecc4a15df docs: README tweaks (#225)
- Add contributors graph
- Add markdownlint config file
- Reformat README line width to 100
- Add a link to the CHANGELOG
- Remove APPS.md
- Change apps link to the Wiki instead of APPS.md
2023-06-08 11:00:18 +02:00
snpefk
e165025c94 docs(readme): remove duplicated mention of tui-rs-tree-widgets (#223) 2023-06-05 23:00:21 +02:00
Orhun Parmaksız
d711f2aef3 chore(ci): integrate cargo-deny for linting dependencies (#221) 2023-06-04 17:32:20 +02:00
Nydragon
e724bec987 chore(commitizen): add commitizen config (#222)
* style(commitizen): add commitizen

add customized commitizen config to match repo needs

implement request mentioned in [#214](https://github.com/tui-rs-revival/ratatui/issues/214) by @joshka

* docs(CONTRIBUTING.md): add a section for the commitizen installation

BREAKING_CHANGE:

* style(commitizen): update breaking change default to false
2023-06-04 17:31:49 +02:00
Josh McKinney
40b3543c3f style(comments): set comment length to wrap at 100 chars (#218)
This is an opinionated default that helps avoid horizontal scrolling.
100 is the most common width on github rust projects and works well for
displaying code on a 16in macbook pro.
2023-06-04 12:34:05 +02:00
Josh McKinney
e95b5127ca docs(lib): fixup tui refs in widgets/mod.rs (#216) 2023-06-04 12:33:09 +02:00
Josh McKinney
5243aa0628 docs(lib): add backend docs (#213) 2023-06-02 19:13:56 +02:00
Yuri Astrakhan
509d18501c chore: lint and doc cleanup (#191)
* chore: Lint and doc cleanup

A few more minor cleanups, mostly in documentation

* Remove unused comment
2023-06-02 16:03:34 +02:00
Josh McKinney
77067bdc58 docs: add CODEOWNERS file (#212)
See https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
2023-06-02 13:21:20 +02:00
Josh McKinney
358b50ba21 build(deps)!: upgrade bitflags to 2.3 (#205)
BREAKING CHANGE: 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
2023-06-01 15:37:59 +02:00
Josh McKinney
b40ca44e1a docs: update README.md and add hello_world example (#204)
- Reformat summary info
- Add badges for dependencies, discord, license
- point existing badges to shields.io
- add Table of Contents
- tweaked installation instructions to show instructions for new and
existing crates
- moved fork status lower
- chop lines generally to 100 limit
- add a quickstart based on a simplified hello_world example
- added / updated some internal links to point locally
- removed some details to simplify the readme (e.g. tick-rate)
- reordered widgets and pointed these at the widget docs
- adds a hello_world example that has just the absolute basic code
necessary to run a ratatui app. This includes some comments that help
guide the user towards other approaches and considerations for a real
world app.
2023-06-01 15:22:18 +02:00
Josh McKinney
a68d621f2d ci: add code coverage action (#209)
This runs the coverage and uploads the output to
https://app.codecov.io/gh/tui-rs-revival/ratatui/
2023-06-01 04:21:16 -07:00
Orhun Parmaksız
21ca38d9e9 chore(release): prepare for 0.21.0 (#197)
* chore(release): prepare for 0.21.0

* chore(changelog): update changelog for latest changes
2023-05-29 12:09:00 +02:00
Josh McKinney
769efc20d1 fix(reflow): remove debug macro call (#198)
This was accidentally left in the previous commit and causes all the
demos to fail.
2023-05-28 21:43:50 +02:00
TimerErTim
32e416ea95 feat(paragraph): allow Lines to be individually aligned (#149)
`Paragraph` now supports rendering each line in the paragraph with a
different alignment (Center, Left and Right) rather than the entire
paragraph being aligned the same. Each line either overrides the
paragraph's alignment or inherits it if the line's alignment is
unspecified.

- Adds `alignment` field to `Line` and builder methods on `Line` and
`Span`
- Updates reflow module types to be line oriented rather than symbol
oriented to take account of each lines alignment
- Adds unit tests to `Paragraph` to fully capture the existing and new
behavior

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2023-05-26 11:58:40 -07:00
Josh McKinney
49e0f4e983 docs: scrape example code from examples/* (#195)
see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html
2023-05-24 12:58:04 -07:00
Yuri Astrakhan
e08b466166 chore: inline format args (#190) 2023-05-21 20:46:02 -07:00
Yuri Astrakhan
3f9935bbcc chore: Minor lints, making Clippy happier (#189)
- `Default::default` is hard to read
- a few `map` -> `map_or`
- simplified `match` -> `let-if`

Signed-off-by: Yuri Astrakhan <YuriAstrakhan@gmail.com>
2023-05-21 20:45:37 -07:00
Orhun Parmaksız
ef8bc7c5a8 feat(border): add border! macro for easy bitflag manipulation (#11)
Adds a `border!` macro that takes TOP, BOTTOM, LEFT, RIGHT, and ALL and
returns a Borders object. An empty `border!()` call returns
Borders::NONE

This is gated behind a `macros` feature flag to ensure short build
times. To enable, add the following to your `Cargo.toml`:

```toml
ratatui = { version = 0.21.0, features = ["macros"] }
```

Co-authored-by: C-Jameson <booksncode@gmail.com>
2023-05-21 15:26:39 -07:00
a-kenji
fa02cf0f2b docs(readme): fix small typo in readme (#186) 2023-05-20 14:59:58 -07:00
Orhun Parmaksız
bc66a27baf feat(widgets): add offset() and offset_mut() for table and list state (#12) 2023-05-20 13:44:33 -07:00
Thomas Mauran
3e54ac3aca style(apps): update the style of application list (#184)
* style(APPS): adding categories

* chore(APPS): remove dots at the end of titles

* chore(APPS): rename to other

* chore(APPS): add description and table of contents

* chore(APPS): change table to list and remove authors

* style(apps): apply formatting

* docs(apps): add the description for termchat

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2023-05-20 20:55:34 +02:00
Amirhossein Akhlaghpour
a425102cf3 feat(color): add FromStr implementation for Color (#180)
* fix: FromStr in Color Struct

* fix: rgb issue

* fix: rgb issue

* fix: add doc

* fix: doctests

* feat: move and add tests

* refactor: rgb & invalid_color test
2023-05-20 18:43:41 +02:00
a-kenji
60cd28005f docs(readme): add termwiz demo to examples (#183) 2023-05-19 19:31:23 +01:00
Josh McKinney
728f82c084 refactor(text): replace Spans with Line (#178)
* refactor: add Line type to replace Spans

`Line` is a significantly better name over `Spans` as the plural causes
confusion and the type really is a representation of a line of text made
up of spans.

This is a backwards compatible version of the approach from
https://github.com/tui-rs-revival/ratatui/pull/175

There is a significant amount of code that uses the Spans type and
methods, so instead of just renaming it, we add a new type and replace
parameters that accepts a `Spans` with a parameter that accepts
`Into<Line>`.

Note that the examples have been intentionally left using `Spans` in
this commit to demonstrate the compiler warnings that will be emitted in
existing code.

Implementation notes:
- moves the Spans code to text::spans and publicly reexports on the text
module. This makes the test in that module only relevant to the Spans
type.
- adds a line module with a copy of the code and tests from Spans with a
single addition: `impl<'a> From<Spans<'a>> for Line<'a>`
- adds tests for `Spans` (created and checked before refactoring)
- adds the same tests for `Line`
- updates all widget methods that accept and store Spans to instead
store `Line` and accept `Into<Line>`

* refactor: move text::Masked to text::masked::Masked

Re-exports the Masked type at text::Masked

* refactor: replace Spans with Line in tests/examples/docs
2023-05-18 20:21:43 +02:00
Orhun Parmaksız
4437835057 feat(backend): add termwiz backend and example (#5)
* build: bump MSRV to 1.65

The latest version of the time crate requires Rust 1.65.0

```
cargo +1.64.0-x86_64-apple-darwin test --no-default-features \
  --features serde,crossterm,all-widgets --lib --tests --examples
error: package `time v0.3.21` cannot be built because it requires rustc
1.65.0 or newer, while the currently active rustc version is 1.64.0
```

* feat(backend): add termwiz backend and demo

* ci(termwiz): add termwiz to makefile.toml

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
Co-authored-by: Prabir Shrestha <mail@prabir.me>
2023-05-12 17:58:01 +02:00
Josh McKinney
1cc405d2dc build: bump MSRV to 1.65.0 (#171)
The latest version of the time crate requires Rust 1.65.0

```
cargo +1.64.0-x86_64-apple-darwin test --no-default-features \
  --features serde,crossterm,all-widgets --lib --tests --examples
error: package `time v0.3.21` cannot be built because it requires rustc
1.65.0 or newer, while the currently active rustc version is 1.64.0
```

Also fixes several clippy warnings added in 1.63/1.65. Although these
have been since moved to nursery / pedantic, it doesn't hurt to fix
these issues as part of this change:

- https://rust-lang.github.io/rust-clippy/master/index.html#derive_partial_eq_without_eq (nursery)
- https://rust-lang.github.io/rust-clippy/master/index.html#bool_to_int_with_if (pedantic)
2023-05-12 17:45:00 +02:00
Josh McKinney
2f0d549a50 feat(text)!: add Masked to display secure data (#168)
Adds a new type Masked that can mask data with a mask character, and can
be used anywhere we expect Cow<'a, str> or Text<'a>. E.g. Paragraph,
ListItem, Table Cells etc.

BREAKING CHANGE:
Because Masked implements From for Text<'a>, code that binds
Into<Text<'a>> without type annotations may no longer compile
(e.g. `Paragraph::new("".as_ref())`)

To fix this, annotate or call to_string() / to_owned() / as_str()
2023-05-09 19:59:24 +02:00
FujiApple
c7aca64ba1 feat(widget): add circle widget (#159) 2023-05-09 19:56:35 +02:00
Josh McKinney
548961f610 test(list): add characterization tests for list (#167)
- also adds builder methods on list state to make it easy to construct
  a list state with selected and offset as a one-liner. Uses `with_` as
  the prefix for these methods as the selected method currently acts as
  a getter rather than a builder.
- cargo tarpaulin suggests only two lines are not covered (the two
  match patterns of the self.start_corner match 223 and 227).
  the body of these lines is covered, so this is probably 100% coverage.
2023-05-09 17:39:01 +02:00
TimerErTim
b3072ce354 style: clippy's variable inlining in format macros 2023-05-07 16:19:34 -07:00
Josh McKinney
98b6b1911c test(buffer): add assert_buffer_eq! and Debug implementation (#161)
- The implementation of Debug is customized to make it easy to use the
output (particularly the content) directly when writing tests (by
surrounding it with `Buffer::with_lines(vec![])`). The styles part of
the message shows the position of every style change, rather than the
style of each cell, which reduces the verbosity of the detail, while
still showing everything necessary to debug the buffer.

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

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

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

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

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

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

* Fix termion demo

* chore: fix all clippy warnings

* Call into_raw_mode()

* Update min supported rust version

---------

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

fix #642

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

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

Closes #569.

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

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

This is a curated addition.

Here are the apps I chose not to add:

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

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

* Undo the capitalization

---------

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

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

* doc: added missing discord invite link
2023-02-13 16:10:08 +01:00
davidhelbig
fafad6c961 chore: fix typo in layout.rs (#619) 2022-08-14 15:58:02 +02:00
Florian Dehau
a4de409235 chore: add apps using tui 2022-08-14 15:51:31 +02:00
Florian Dehau
a05fd45959 Release v0.19.0 2022-08-14 15:38:31 +02:00
Florian Dehau
24de2f8a96 chore: bump crossterm to v0.25 2022-08-14 15:19:48 +02:00
Florian Dehau
eee37011a5 chore: fix clippy warnings 2022-08-14 14:59:04 +02:00
Florian Dehau
a67706bea0 chore(ci): bump cargo-make to v0.35.16 2022-08-14 14:43:38 +02:00
Linda_pp
faa69b6cfe chore: explicitly set MSRV to 1.56.1 in Cargo.toml 2022-08-14 14:24:54 +02:00
Florian Dehau
ba5ea2deff chore: update README 2022-08-14 13:58:53 +02:00
♫ Christian Krause ♫
a6b25a4877 chore: add panic hook example (#593)
Without a terminal-resetting panic hook there are two main problems when
an application panics:

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

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

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

* refactor(chart): rename ambiguous function parameter

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

* refactor(chart): refactor axis test cases

* refactor(chart): rename minor variable

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

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

* refactor(chart): rename alignment property

* refactor(chart): merge two nested conditions

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

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

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

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

* Added test for inner with title in block

* Updated block example to show center alignment

* Formatting fixed

* Updated tests to use lamdas and be more concise. Updated title alignmnet code to be more straightforward and have correct behavior when placing title in the center without left border
2021-05-22 16:55:24 +02:00
Andrew Chin
24396d97ed doc: Add doctests that shows how Text can be created from Cow<str> 2021-05-22 16:47:06 +02:00
Andrew Chin
703e41cd49 feat(Text): Add a From<Cow<str>> impl for Text 2021-05-22 16:47:06 +02:00
Florian Dehau
975c4165d0 chore: fix clippy warnings 2021-05-22 15:54:38 +02:00
Arijit Basu
dbf38d847a Add xplr to the "apps using tui" list
`xplr` is a hackable TUI file explorer.
2021-05-22 15:08:06 +02:00
Florian Dehau
91a2519cc3 chore: update links to examples in README
Links now include the fully qualified domain as well as the version.
This will make them work in docs.rs and make sure readers are looking at code which is consistent with the latest version available.
2021-05-03 00:30:59 +02:00
Alexandru Scvortov
a1c3ba2088 fix: actually clear buffer in TestBackend::clear (#461) 2021-05-02 22:35:27 +02:00
Alexandru Scvortov
d47565be5c fix: actually clear buffer in TestBackend::clear (#461) 2021-05-02 22:35:08 +02:00
Florian Dehau
1028d39db0 chore: improve contributing guidelines
* Improve issue templates and make them mandatory.
* Improve CONTRIBUTING.md.
* Add template for pull requests.
2021-05-02 21:42:31 +02:00
Deepu K Sasidharan
b250825c38 Add kdash to apps using this section (#469)
chore: add `kdash` to apps using `tui`
2021-05-02 20:13:56 +02:00
Florian Dehau
90a6a8f2d6 Release v0.15.0 2021-05-02 19:03:01 +02:00
Florian Dehau
414386e797 chore: update rand to 0.8 (#472) 2021-05-02 18:51:59 +02:00
Joey Ezechiëls
3a843d5074 fix(test): remove compile warning in TestBackend::assert_buffer (#466) 2021-05-02 18:27:47 +02:00
Luc Perkins
4e76bfa2ca chore: add Vector to list of apps using tui (#452)
Signed-off-by: Luc Perkins <luc@timber.io>
2021-02-21 14:58:57 +01:00
Simas Toleikis
8832281dcf Update crossterm to 0.19. 2021-01-04 23:26:04 +01:00
Florian Dehau
853f3d9200 feat(terminal): add a read-only view of the terminal state after the draw call 2021-01-04 22:18:28 +01:00
Florian Dehau
67e996c5f4 feat(examples): add third tab to demo to show colors 2021-01-01 15:41:49 +01:00
Florian Dehau
f09863faa0 Release v0.14.0 2021-01-01 14:51:08 +01:00
Florian Dehau
eb1e3be722 fix(widgets/block): make Block::inner return more accurate results on small areas 2020-12-13 17:21:10 +01:00
Sagie Gur-Ari
4ec902b96f chore: make run-examples available on all platforms (#429)
* Make examples available for all platforms
* limit windows to crossterm_demo only and make q exit demos work
2020-12-13 15:29:31 +01:00
Vadim Chekan
74243394d9 fix(widgets/table): draw table header and border even if rows are empty (#426) 2020-12-08 21:49:16 +01:00
Florian Dehau
e7f263efa7 chore(ci): fix cargo-make cache on windows runner 2020-12-07 23:58:27 +01:00
Florian Dehau
0991145c58 chore(ci): simplify ci workflow (#428)
* chore(ci): simplify ci workflow

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

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

* removed unnecessary if

* changed function name to better reflect unicode

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

* formatting

* improved readability

* gauge tests now check color

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

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

* Fix rounding of cassowary solved widths to eliminate imprecisions

* Fix formatting to fit convention

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

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

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

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

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

Fixes #373
2020-09-20 14:54:16 +02:00
Cory Forsstrom
aada695b3f chore: add tickrs to apps using tui (#351) 2020-08-23 21:20:30 +02:00
Florian Dehau
90f3858eff feat(backend): keep the internal buffers in sync when the terminal is cleared. 2020-08-23 21:13:12 +02:00
Florian Dehau
ecb482f297 fix(backend): move the cursor when first diff is on second cell
Both termion and crossterm backends were not moving the cursor if the first diff to draw was on the
second cell. The condition triggering the cursor move has been updated to fix this. In addition, two
tests have been added to avoid future regressions.
2020-08-02 21:10:44 +02:00
Florian Dehau
641f391137 feat(terminal): add unstable api to use a fixed viewport
There was now way to avoid the autoresize behavior of `Terminal`. While it was fine for most users,
it made the testing experience painful as it was impossible to avoid the calls to `Backend::size()`.
Indeed they trigger the following error: "Inappropriate ioctl for device" since we are not running
the tests in a real terminal (at least in the CI).

This commit introduces a new api to create a `Terminal` with a fixed viewport.
2020-08-02 20:46:57 +02:00
Florian Dehau
dc26f7ba9f chore: document rustc min version supported
- Add section to README
- Run ci tests with this min version in addition of stable to track changes that would require a min
rustc version bump.
2020-08-02 16:37:34 +02:00
Florian Dehau
6504930888 Release v0.10.0 2020-07-18 18:03:32 +02:00
Florian Dehau
6b52c91257 chore: update CHANGELOG 2020-07-17 22:05:37 +02:00
Florian Dehau
0ffea495b1 refactor: implement cascading styles
- merge `Style` and `StyleDiff` together. `Style` now is used to activate or deactivate certain
style rules not to overidden all of them.
- update all impacted widgets, examples and tests.
2020-07-17 20:58:20 +02:00
Florian Dehau
72ba4ff2d4 refactor(examples): remove unecessary terminal.hide_cursor calls 2020-07-10 23:56:46 +02:00
Florian Dehau
88c4b191fb feat(text): add new text primitives 2020-07-10 22:59:24 +02:00
Brooks Rady
112d2a65f6 feat(widgets/paragraph): add option to preserve indentation when the text is wrapped (#327) 2020-07-07 00:10:24 +02:00
Xiaopeng Li
d999c1b434 feat(widgets/paragraph): Add horizontal scroll (#329)
* `Paragraph:scroll` takes a tuple of offsets instead of a single vertical offset.
* `LineTruncator` takes this new horizontal offset into account to let the paragraph scroll horizontally.
2020-07-06 23:47:52 +02:00
Mikko Rantanen
3aa8b9a259 Implement patch between two StyleDiff 2020-07-05 18:32:33 +02:00
Florian Dehau
fdbea9e2ee fix(widgets/canvas): avoid panic on zero-width bounds 2020-07-05 14:02:11 +02:00
Priime
6204eddade fix(readme): typo in demo section
There was a very small typo in the README on line 40, which cited the
`examples` folder as `exmples`. This resolves that issue.
2020-07-05 11:08:19 +02:00
Aram Drevekenin
e789c671b0 Update README.md 2020-06-28 15:23:22 +02:00
Alexander Batischev
8c2ee0ed85 feat(terminal): Add after-draw() cursor control to Frame (#91) (#309) 2020-06-15 22:57:23 +02:00
Cokemonkey11
2b48409cfd fix(examples): remove typo in demo text 2020-06-15 22:46:45 +02:00
Florian Dehau
7251186762 feat(style): add StyleDiff 2020-06-08 23:55:23 +02:00
Florian Dehau
82fda4ac0e doc(style): improve documentation of Style 2020-06-08 23:55:23 +02:00
Kenta Iwasaki
1d12ddbdfc layout: add vertical split constraint test on height 2020-06-08 23:05:08 +02:00
Kenta Iwasaki
f474c76e19 layout: force constraint that width and height are non-negative 2020-06-08 23:05:08 +02:00
Florian Dehau
ac99104114 feat(style): add support to serialize and deserialize Style using serde
* Add serde as an optional dependency.
* Add feature-gated derives to Color, Modifier and Style.
2020-05-28 01:08:40 +02:00
Paul Horn
0bb9b388f7 Borrow layout for splitting instead of moving it
This allows for layouts to be saved and reused and does not require an additional clone
2020-05-28 00:06:47 +02:00
Florian Dehau
b59e4bb808 feat(examples): enable mouse capture to make crossterm demo on par with termion 2020-05-27 23:56:46 +02:00
Florian Dehau
4fe647df0a refactor(tests): rename integration tests to be able to call group of tests 2020-05-21 21:59:39 +02:00
Florian Dehau
a00350ab54 refactor(tests): rename test files and use the new TestBackend::assert_buffer method 2020-05-21 21:59:39 +02:00
Florian Dehau
96c6b4efcb refactor(tests): move test utilities to TestBackend
* Remove custom Debug implementation of Buffer
* Add `TestBackend::assert_buffer` to compare buffers in integration tests. When
the assertion fails, the output now show the list of differences in addition
of the views of the computed and expected buffers. This effectively replaces
the table of debug code for colors and modifiers as it is easier to read.
2020-05-21 21:59:39 +02:00
Florian Dehau
18714caa60 Release v0.9.5 2020-05-21 20:59:34 +02:00
Stephan Dilly
7110fe0159 fix panic on narrow buffers (fixes #293) 2020-05-21 19:14:12 +02:00
Florian Dehau
5a590bca74 chore: enable clippy on all targets and all features
- Remove deny warnings in lib.rs. This allows easier iteration when developing
new features. The warnings will make the CI fails anyway on the clippy CI
stage.
- Run clippy on all targets (including tests and examples) and all features.
- Fail CI on clippy warnings.
2020-05-17 01:07:49 +02:00
Florian Dehau
963f11a6b1 Release v0.9.4 2020-05-12 21:25:28 +02:00
Florian Dehau
a7761fe55d fix(buffer): ignore zero-width graphemes 2020-05-12 21:16:16 +02:00
Florian Dehau
10cf9305f1 Release v0.9.3 2020-05-11 00:02:12 +02:00
Clement Tsang
b72ced4511 fix(widgets/chart): remove overflow when dataset if empty (#274)
* docs: Fix missing code block fence
* use slice::windows to deal with underflow issue
* add test for empty dataset and lines
2020-05-10 23:48:12 +02:00
252 changed files with 58818 additions and 8315 deletions

78
.cz.toml Normal file
View File

@@ -0,0 +1,78 @@
# configuration for https://github.com/commitizen/cz-cli
[tool.commitizen]
name = "cz_customize"
tag_format = "$version"
version_type = "semver"
version_provider = "cargo"
update_changelog_on_bump = true
major_version_zero = true
use_shortcuts = true
[tool.commitizen.customize]
message_template = """{{change_type}}({{scope}}): {{subject}}
{% if body %}\
{{body}}\
{% endif %}
{%if is_breaking_change %}\
BREAKING_CHANGE: \
{% endif %}\
{{footer}}\
"""
example = "feature: this feature enable customize through config file"
schema = "<type>(<scope>): <subject>\n\n<body>\n\n<footer>"
schema_pattern = "(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)\\(\\w+\\):\\s(?P<subject>.*)(\\n\\n(?P<body>.*))?(\\n\\n(?P<footer>.*))?"
# The order needs to be preserved, as it influences the order when executing cz commit/cz c
# Change types
[[tool.commitizen.customize.questions]]
type = "list"
name = "change_type"
choices = [
{ value = "build", name = "build: Changes that affect the build system or external dependencies (example scopes: pip, docker, npm)", key = "b" },
{ value = "chore", name = "chore: A modification that generally does not fall into any other category", key = "c" },
{ value = "ci", name = "ci: Changes to our CI configuration files and scripts (example scopes: GitLabCI)", key = "i" },
{ value = "docs", name = "docs: Documentation only changes", key = "d" },
{ value = "feat", name = "feat: A new feature.", key = "f" },
{ value = "fix", name = "fix: A bug fix.", key = "x" },
{ value = "perf", name = "perf: A code change that improves performance", key = "p" },
{ value = "refactor", name = "refactor: A code change that neither fixes a bug nor adds a feature", key = "r" },
{ value = "revert", name = "revert: Revert previous commits", key = "v" },
{ value = "style", name = "style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)", key = "s" },
{ value = "test", name = "test: Adding missing or correcting existing tests", key = "t" },
]
message = "Select the type of change you are committing"
# The scope of the change, can be a file, class name or other context
[[tool.commitizen.customize.questions]]
type = "input"
name = "scope"
message = "What is the scope of this change? (class or file name): (press [enter] to skip)\n"
# Summary of the changes
[[tool.commitizen.customize.questions]]
"type" = "input"
"name" = "subject"
"message" = "Write a short and imperative summary of the code changes: (lower case and no period)\n"
# The commit body, elaborate the changes if need be.
[[tool.commitizen.customize.questions]]
type = "input"
name = "body"
message = "Provide additional contextual information about the code changes: (press [enter] to skip)\n"
# Specify if the changes are breaking
[[tool.commitizen.customize.questions]]
type = "confirm"
name = "is_breaking_change"
message = "Is this a BREAKING CHANGE?"
default = false
# Reference closing issues and share other
[[tool.commitizen.customize.questions]]
type = "input"
name = "footer"
message = "Footer. Information about Breaking Changes and reference issues that this commit closes: (press [enter] to skip)"

17
.editorconfig Normal file
View File

@@ -0,0 +1,17 @@
# configuration for https://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.rs]
indent_style = space
indent_size = 4
[*.yml]
indent_style = space
indent_size = 2

11
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,11 @@
# See <https://help.github.com/articles/about-codeowners/>
# for more info about CODEOWNERS file
# It uses the same pattern rule for gitignore file
# <https://git-scm.com/docs/gitignore#_pattern_format>
# Maintainers
* @ratatui/maintainers

View File

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

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

@@ -0,0 +1,14 @@
blank_issues_enabled: false
contact_links:
- name: Frequently Asked Questions
url: https://ratatui.rs/faq/
about: Check the website FAQ section to see if your question has already been answered
- name: Ratatui Forum
url: https://forum.ratatui.rs
about: Ask questions about ratatui on our Forum
- 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

View File

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

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

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

@@ -0,0 +1 @@
<!-- Please read CONTRIBUTING.md before submitting any pull request. -->

25
.github/workflows/bench_base.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Run Benchmarks
on:
push:
branches:
- main
jobs:
benchmark_base_branch:
name: Continuous Benchmarking with Bencher
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: bencherdev/bencher@main
- name: Track base branch benchmarks with Bencher
run: |
bencher run \
--project ratatui-org \
--token '${{ secrets.BENCHER_API_TOKEN }}' \
--branch main \
--testbed ubuntu-latest \
--adapter rust_criterion \
--err \
cargo bench

25
.github/workflows/bench_run_fork_pr.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Run and Cache Benchmarks
on:
pull_request:
types: [opened, reopened, edited, synchronize]
jobs:
benchmark_fork_pr_branch:
name: Run Fork PR Benchmarks
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run Benchmarks
run: cargo bench > benchmark_results.txt
- name: Upload Benchmark Results
uses: actions/upload-artifact@v4
with:
name: benchmark_results.txt
path: ./benchmark_results.txt
- name: Upload GitHub Pull Request Event
uses: actions/upload-artifact@v4
with:
name: event.json
path: ${{ github.event_path }}

View File

@@ -0,0 +1,75 @@
name: Track Benchmarks with Bencher
on:
workflow_run:
workflows: [Run and Cache Benchmarks]
types: [completed]
permissions:
contents: read
pull-requests: write
jobs:
track_fork_pr_branch:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
env:
BENCHMARK_RESULTS: benchmark_results.txt
PR_EVENT: event.json
steps:
- name: Download Benchmark Results
uses: actions/github-script@v7
with:
script: |
async function downloadArtifact(artifactName) {
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == artifactName
})[0];
if (!matchArtifact) {
core.setFailed(`Failed to find artifact: ${artifactName}`);
}
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/${artifactName}.zip`, Buffer.from(download.data));
}
await downloadArtifact(process.env.BENCHMARK_RESULTS);
await downloadArtifact(process.env.PR_EVENT);
- name: Unzip Benchmark Results
run: |
unzip $BENCHMARK_RESULTS.zip
unzip $PR_EVENT.zip
- name: Export PR Event Data
uses: actions/github-script@v7
with:
script: |
let fs = require('fs');
let prEvent = JSON.parse(fs.readFileSync(process.env.PR_EVENT, {encoding: 'utf8'}));
core.exportVariable("PR_HEAD", `${prEvent.number}/merge`);
core.exportVariable("PR_BASE", prEvent.pull_request.base.ref);
core.exportVariable("PR_BASE_SHA", prEvent.pull_request.base.sha);
core.exportVariable("PR_NUMBER", prEvent.number);
- uses: bencherdev/bencher@main
- name: Track Benchmarks with Bencher
run: |
bencher run \
--project ratatui-org \
--token '${{ secrets.BENCHER_API_TOKEN }}' \
--branch '${{ env.PR_HEAD }}' \
--branch-start-point '${{ env.PR_BASE }}' \
--branch-start-point-hash '${{ env.PR_BASE_SHA }}' \
--testbed ubuntu-latest \
--adapter rust_criterion \
--err \
--github-actions '${{ secrets.GITHUB_TOKEN }}' \
--ci-number '${{ env.PR_NUMBER }}' \
--file "$BENCHMARK_RESULTS"

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 -P "v0+\.\d+\.\d+$" | tail -1)"
echo "🐭 Last release: ${last_release}"
# detect breaking changes
if [ -n "$(git log --oneline ${last_release}..HEAD | grep '!:')" ]; 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}"

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

@@ -0,0 +1,86 @@
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: ['Type: 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

16
.github/workflows/check-semver.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: Check Semver
on:
pull_request:
branches:
- main
jobs:
check-semver:
name: Check semver
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4
- name: Check semver
uses: obi1kenobi/cargo-semver-checks-action@v2

View File

@@ -1,70 +1,160 @@
on: [push, pull_request]
name: Continuous Integration
name: CI
on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
push:
branches:
- main
pull_request:
branches:
- main
merge_group:
# ensure that the workflow is only triggered once per PR, subsequent pushes to the PR will cancel
# and restart the workflow. See https://docs.github.com/en/actions/using-jobs/using-concurrency
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
# lint, clippy and coverage jobs are intentionally early in the workflow to catch simple formatting,
# typos, and missing tests as early as possible. This allows us to fix these and resubmit the PR
# without having to wait for the comprehensive matrix of tests to complete.
jobs:
linux:
name: Linux
rustfmt:
runs-on: ubuntu-latest
steps:
- name: "Install dependencies"
run: sudo apt-get install libncurses5-dev
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
with:
profile: default
toolchain: stable
override: true
- name: "Format"
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- name: "Check"
uses: actions-rs/cargo@v1
with:
command: check
args: --examples
- name: "Check (crossterm)"
uses: actions-rs/cargo@v1
with:
command: check
args: --no-default-features --features=crossterm --example crossterm_demo
- name: "Check (rustbox)"
uses: actions-rs/cargo@v1
with:
command: test
args: --no-default-features --features=rustbox --example rustbox_demo
- name: "Check (curses)"
uses: actions-rs/cargo@v1
with:
command: check
args: --no-default-features --features=curses --example curses_demo
- name: "Test"
uses: actions-rs/cargo@v1
with:
command: test
- name: "Clippy"
uses: actions-rs/cargo@v1
with:
command: clippy
windows:
name: Windows
runs-on: windows-latest
components: rustfmt
- run: cargo +nightly fmt --all --check
typos:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
- uses: actions/checkout@v4
- uses: crate-ci/typos@master
dependencies:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
cargo-machete:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- uses: bnjbvr/cargo-machete@v0.6.2
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
profile: default
toolchain: stable
override: true
- name: "Check (crossterm)"
uses: actions-rs/cargo@v1
components: clippy
- uses: taiki-e/install-action@cargo-make
- uses: Swatinem/rust-cache@v2
- run: cargo make clippy
markdownlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: DavidAnson/markdownlint-cli2-action@v17
with:
command: check
args: --no-default-features --features=crossterm --example crossterm_demo
- name: "Test (crossterm)"
uses: actions-rs/cargo@v1
globs: |
'**/*.md'
'!target'
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
command: test
args: --no-default-features --features=crossterm --tests --examples
components: llvm-tools
- uses: taiki-e/install-action@v2
with:
tool: cargo-llvm-cov,cargo-make
- uses: Swatinem/rust-cache@v2
- run: cargo make coverage
- uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
check:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
toolchain: ["1.74.0", "stable"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
- uses: taiki-e/install-action@cargo-make
- uses: Swatinem/rust-cache@v2
- run: cargo make check
env:
RUST_BACKTRACE: full
lint-docs:
runs-on: ubuntu-latest
env:
RUSTDOCFLAGS: -Dwarnings
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- uses: dtolnay/install@cargo-docs-rs
- uses: Swatinem/rust-cache@v2
# Run cargo rustdoc with the same options that would be used by docs.rs, taking into account
# the package.metadata.docs.rs configured in Cargo.toml.
# https://github.com/dtolnay/cargo-docs-rs
- run: cargo +nightly docs-rs
test-doc:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@cargo-make
- uses: Swatinem/rust-cache@v2
- run: cargo make test-doc
env:
RUST_BACKTRACE: full
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
toolchain: ["1.74.0", "stable"]
backend: [crossterm, termion, termwiz]
exclude:
# termion is not supported on windows
- os: windows-latest
backend: termion
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
- uses: taiki-e/install-action@v2
with:
tool: cargo-make,nextest
- uses: Swatinem/rust-cache@v2
- run: cargo make test-backend ${{ matrix.backend }}
env:
RUST_BACKTRACE: full

48
.github/workflows/release-alpha.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
name: Release alpha version
on:
workflow_dispatch:
schedule:
# At 00:00 on Saturday
# https://crontab.guru/#0_0_*_*_6
- cron: "0 0 * * 6"
defaults:
run:
shell: bash
jobs:
publish-alpha:
name: Create an alpha release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Calculate the next release
run: .github/workflows/calculate-alpha-release.bash
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Publish
run: cargo publish --allow-dirty --token ${{ secrets.CARGO_TOKEN }}
- name: Generate a changelog
uses: orhun/git-cliff-action@v4
with:
config: cliff.toml
args: --unreleased --tag ${{ env.NEXT_TAG }} --strip header
env:
OUTPUT: BODY.md
- name: Publish on GitHub
uses: ncipollo/release-action@v1
with:
tag: ${{ env.NEXT_TAG }}
prerelease: true
bodyFile: BODY.md

45
.github/workflows/release-stable.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Release stable version
on:
push:
tags:
- "v*.*.*"
jobs:
publish-stable:
name: Create an stable release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate a changelog
uses: orhun/git-cliff-action@v4
with:
config: cliff.toml
args: --latest --strip header
env:
OUTPUT: BODY.md
- name: Publish on GitHub
uses: ncipollo/release-action@v1
with:
prerelease: false
bodyFile: BODY.md
publish-crate:
name: Publish crate
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Publish
run: cargo publish --token ${{ secrets.CARGO_TOKEN }}

15
.markdownlint.yaml Normal file
View File

@@ -0,0 +1,15 @@
# 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
# to support repeated headers in the changelog
no-duplicate-heading: false

699
BREAKING-CHANGES.md Normal file
View File

@@ -0,0 +1,699 @@
# 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/ratatui/issues?q=label%3A%22breaking+change%22)
## Summary
This is a quick summary of the sections below:
- [v0.28.0](#v0280)
`Backend::size` returns `Size` instead of `Rect`
- `Backend` trait migrates to `get/set_cursor_position`
- Ratatui now requires Crossterm 0.28.0
- `Axis::labels` now accepts `IntoIterator<Into<Line>>`
- `Layout::init_cache` no longer returns bool and takes a `NonZeroUsize` instead of `usize`
- `ratatui::terminal` module is now private
- `ToText` no longer has a lifetime
- `Frame::size` is deprecated and renamed to `Frame::area`
- [v0.27.0](#v0270)
- List no clamps the selected index to list
- Prelude items added / removed
- 'termion' updated to 4.0
- `Rect::inner` takes `Margin` directly instead of reference
- `Buffer::filled` takes `Cell` directly instead of reference
- `Stylize::bg()` now accepts `Into<Color>`
- Removed deprecated `List::start_corner`
- `LineGauge::gauge_style` is deprecated
- [v0.26.0](#v0260)
- `Flex::Start` is the new default flex mode for `Layout`
- `patch_style` & `reset_style` now consume and return `Self`
- Removed deprecated `Block::title_on_bottom`
- `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
- `Tabs::new()` now accepts `IntoIterator<Item: Into<Line<'a>>>`
- `Table::new` now accepts `IntoIterator<Item: Into<Row<'a>>>`.
- [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.28.0
### `Backend::size` returns `Size` instead of `Rect` ([#1254])
[#1254]: https://github.com/ratatui/ratatui/pull/1254
The `Backend::size` method returns a `Size` instead of a `Rect`.
There is no need for the position here as it was always 0,0.
### `Backend` trait migrates to `get/set_cursor_position` ([#1284])
[#1284]: https://github.com/ratatui/ratatui/pull/1284
If you just use the types implementing the `Backend` trait, you will see deprecation hints but
nothing is a breaking change for you.
If you implement the Backend trait yourself, you need to update the implementation and add the
`get/set_cursor_position` method. You can remove the `get/set_cursor` methods as they are deprecated
and a default implementation for them exists.
### Ratatui now requires Crossterm 0.28.0 ([#1278])
[#1278]: https://github.com/ratatui/ratatui/pull/1278
Crossterm is updated to version 0.28.0, which is a semver incompatible version with the previous
version (0.27.0). Ratatui re-exports the version of crossterm that it is compatible with under
`ratatui::crossterm`, which can be used to avoid incompatible versions in your dependency list.
### `Axis::labels()` now accepts `IntoIterator<Into<Line>>` ([#1273] and [#1283])
[#1273]: https://github.com/ratatui/ratatui/pull/1173
[#1283]: https://github.com/ratatui/ratatui/pull/1283
Previously Axis::labels accepted `Vec<Span>`. Any code that uses conversion methods that infer the
type will need to be rewritten as the compiler cannot infer the correct type.
```diff
- Axis::default().labels(vec!["a".into(), "b".into()])
+ Axis::default().labels(["a", "b"])
```
### `Layout::init_cache` no longer returns bool and takes a `NonZeroUsize` instead of `usize` ([#1245])
[#1245]: https://github.com/ratatui/ratatui/pull/1245
```diff
- let is_initialized = Layout::init_cache(100);
+ Layout::init_cache(NonZeroUsize::new(100).unwrap());
```
### `ratatui::terminal` module is now private ([#1160])
[#1160]: https://github.com/ratatui/ratatui/pull/1160
The `terminal` module is now private and can not be used directly. The types under this module are
exported from the root of the crate. This reduces clashes with other modules in the backends that
are also named terminal, and confusion about module exports for newer Rust users.
```diff
- use ratatui::terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
+ use ratatui::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
```
### `ToText` no longer has a lifetime ([#1234])
[#1234]: https://github.com/ratatui/ratatui/pull/1234
This change simplifies the trait and makes it easier to implement.
### `Frame::size` is deprecated and renamed to `Frame::area`
[#1293]: https://github.com/ratatui/ratatui/pull/1293
`Frame::size` is renamed to `Frame::area` as it's the more correct name.
## [v0.27.0](https://github.com/ratatui/ratatui/releases/tag/v0.27.0)
### List no clamps the selected index to list ([#1159])
[#1149]: https://github.com/ratatui/ratatui/pull/1149
The `List` widget now clamps the selected index to the bounds of the list when navigating with
`first`, `last`, `previous`, and `next`, as well as when setting the index directly with `select`.
Previously selecting an index past the end of the list would show treat the list as having a
selection which was not visible. Now the last item in the list will be selected instead.
### Prelude items added / removed ([#1149])
The following items have been removed from the prelude:
- `style::Styled` - this trait is useful for widgets that want to
support the Stylize trait, but it adds complexity as widgets have two
`style` methods and a `set_style` method.
- `symbols::Marker` - this item is used by code that needs to draw to
the `Canvas` widget, but it's not a common item that would be used by
most users of the library.
- `terminal::{CompletedFrame, TerminalOptions, Viewport}` - these items
are rarely used by code that needs to interact with the terminal, and
they're generally only ever used once in any app.
The following items have been added to the prelude:
- `layout::{Position, Size}` - these items are used by code that needs
to interact with the layout system. These are newer items that were
added in the last few releases, which should be used more liberally.
This may cause conflicts for types defined elsewhere with a similar
name.
To update your app:
```diff
// if your app uses Styled::style() or Styled::set_style():
-use ratatui::prelude::*;
+use ratatui::{prelude::*, style::Styled};
// if your app uses symbols::Marker:
-use ratatui::prelude::*;
+use ratatui::{prelude::*, symbols::Marker}
// if your app uses terminal::{CompletedFrame, TerminalOptions, Viewport}
-use ratatui::prelude::*;
+use ratatui::{prelude::*, terminal::{CompletedFrame, TerminalOptions, Viewport}};
// to disambiguate existing types named Position or Size:
- use some_crate::{Position, Size};
- let size: Size = ...;
- let position: Position = ...;
+ let size: some_crate::Size = ...;
+ let position: some_crate::Position = ...;
```
### Termion is updated to 4.0 [#1106]
Changelog: <https://gitlab.redox-os.org/redox-os/termion/-/blob/master/CHANGELOG.md>
A change is only necessary if you were matching on all variants of the `MouseEvent` enum without a
wildcard. In this case, you need to either handle the two new variants, `MouseLeft` and
`MouseRight`, or add a wildcard.
[#1106]: https://github.com/ratatui/ratatui/pull/1106
### `Rect::inner` takes `Margin` directly instead of reference ([#1008])
[#1008]: https://github.com/ratatui/ratatui/pull/1008
`Margin` needs to be passed without reference now.
```diff
-let area = area.inner(&Margin {
+let area = area.inner(Margin {
vertical: 0,
horizontal: 2,
});
```
### `Buffer::filled` takes `Cell` directly instead of reference ([#1148])
[#1148]: https://github.com/ratatui/ratatui/pull/1148
`Buffer::filled` moves the `Cell` instead of taking a reference.
```diff
-Buffer::filled(area, &Cell::new("X"));
+Buffer::filled(area, Cell::new("X"));
```
### `Stylize::bg()` now accepts `Into<Color>` ([#1103])
[#1103]: https://github.com/ratatui/ratatui/pull/1103
Previously, `Stylize::bg()` accepted `Color` but now accepts `Into<Color>`. This allows more
flexible types from calling scopes, though it can break some type inference in the calling scope.
### Remove deprecated `List::start_corner` and `layout::Corner` ([#759])
[#759]: https://github.com/ratatui/ratatui/pull/759
`List::start_corner` was deprecated in v0.25. Use `List::direction` and `ListDirection` instead.
```diff
- list.start_corner(Corner::TopLeft);
- list.start_corner(Corner::TopRight);
// This is not an error, BottomRight rendered top to bottom previously
- list.start_corner(Corner::BottomRight);
// all becomes
+ list.direction(ListDirection::TopToBottom);
```
```diff
- list.start_corner(Corner::BottomLeft);
// becomes
+ list.direction(ListDirection::BottomToTop);
```
`layout::Corner` was removed entirely.
### `LineGauge::gauge_style` is deprecated ([#565])
[#565]: https://github.com/ratatui/ratatui/pull/1148
`LineGauge::gauge_style` is deprecated and replaced with `LineGauge::filled_style` and `LineGauge::unfilled_style`:
```diff
let gauge = LineGauge::default()
- .gauge_style(Style::default().fg(Color::Red).bg(Color::Blue)
+ .filled_style(Style::default().fg(Color::Green))
+ .unfilled_style(Style::default().fg(Color::White));
```
## [v0.26.0](https://github.com/ratatui/ratatui/releases/tag/v0.26.0)
### `Flex::Start` is the new default flex mode for `Layout` ([#881])
[#881]: https://github.com/ratatui/ratatui/pull/881
Previously, constraints would stretch to fill all available space, violating constraints if
necessary.
With v0.26.0, `Flex` modes are introduced, and the default is `Flex::Start`, which will align
areas associated with constraints to be beginning of the area. With v0.26.0, additionally,
`Min` constraints grow to fill excess space. These changes will allow users to build layouts
more easily.
With v0.26.0, users will most likely not need to change what constraints they use to create
existing layouts with `Flex::Start`. However, to get old behavior, use `Flex::Legacy`.
```diff
- let rects = Layout::horizontal([Length(1), Length(2)]).split(area);
// becomes
+ let rects = Layout::horizontal([Length(1), Length(2)]).flex(Flex::Legacy).split(area);
```
### `Table::new()` now accepts `IntoIterator<Item: Into<Row<'a>>>` ([#774])
[#774]: https://github.com/ratatui/ratatui/pull/774
Previously, `Table::new()` accepted `IntoIterator<Item=Row<'a>>`. The argument change to
`IntoIterator<Item: Into<Row<'a>>>`, This allows more flexible types from calling scopes, though it
can some break type inference in the calling scope for empty containers.
This can be resolved either by providing an explicit type (e.g. `Vec::<Row>::new()`), or by using
`Table::default()`.
```diff
- let table = Table::new(vec![], widths);
// becomes
+ let table = Table::default().widths(widths);
```
### `Tabs::new()` now accepts `IntoIterator<Item: Into<Line<'a>>>` ([#776])
[#776]: https://github.com/ratatui/ratatui/pull/776
Previously, `Tabs::new()` accepted `Vec<T>` where `T: Into<Line<'a>>`. This allows more flexible
types from calling scopes, though it can break some type inference in the calling scope.
This typically occurs when collecting an iterator prior to calling `Tabs::new`, and can be resolved
by removing the call to `.collect()`.
```diff
- let tabs = Tabs::new((0.3).map(|i| format!("{i}")).collect());
// becomes
+ let tabs = Tabs::new((0.3).map(|i| format!("{i}")));
```
### Table::default() now sets segment_size to None and column_spacing to ([#751])
[#751]: https://github.com/ratatui/ratatui/pull/751
The default() implementation of Table now sets the column_spacing field to 1 and the segment_size
field to `SegmentSize::None`. This will affect the rendering of a small amount of apps.
To use the previous default values, call `table.segment_size(Default::default())` and
`table.column_spacing(0)`.
### `patch_style` & `reset_style` now consumes and returns `Self` ([#754])
[#754]: https://github.com/ratatui/ratatui/pull/754
Previously, `patch_style` and `reset_style` in `Text`, `Line` and `Span` were using a mutable
reference to `Self`. To be more consistent with the rest of `ratatui`, which is using fluent
setters, these now take ownership of `Self` and return it.
The following example shows how to migrate for `Line`, but the same applies for `Text` and `Span`.
```diff
- let mut line = Line::from("foobar");
- line.patch_style(style);
// becomes
+ let line = Line::new("foobar").patch_style(style);
```
### Remove deprecated `Block::title_on_bottom` ([#757])
`Block::title_on_bottom` was deprecated in v0.22. Use `Block::title` and `Title::position` instead.
```diff
- block.title("foobar").title_on_bottom();
+ block.title(Title::from("foobar").position(Position::Bottom));
```
### `Block` style methods cannot be used in a const context ([#720])
[#720]: https://github.com/ratatui/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/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::styled` 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/ratatui/releases/tag/v0.25.0)
### Removed `Axis::title_style` and `Buffer::set_background` ([#691])
[#691]: https://github.com/ratatui/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/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/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/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/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/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/ratatui/releases/tag/v0.24.0)
### `ScrollbarState` field type changed from `u16` to `usize` ([#456])
[#456]: https://github.com/ratatui/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/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/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/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/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/ratatui/releases/tag/v0.23.0)
### `Scrollbar::track_symbol()` now takes an `Option<&str>` instead of `&str` ([#360])
[#360]: https://github.com/ratatui/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/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/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/ratatui/releases/tag/v0.22.0)
### `bitflags` updated to 2.3 ([#205])
[#205]: https://github.com/ratatui/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/ratatui/releases/tag/v0.21.0)
### MSRV is 1.65.0 ([#171])
[#171]: https://github.com/ratatui/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/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/ratatui/issues/168
A new type `Masked` was introduced that implements `From<Text<'a>>`. This causes any code that
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/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/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/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/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

128
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
https://forum.ratatui.rs/ or https://discord.gg/pMCEU9hNEj.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

211
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,211 @@
# Contribution guidelines
First off, thank you for considering contributing to Ratatui.
If your contribution is not straightforward, please first discuss the change you wish to make by
creating a new issue before making the change, or starting a discussion on
[discord](https://discord.gg/pMCEU9hNEj).
## Reporting issues
Before reporting an issue on the [issue tracker](https://github.com/ratatui/ratatui/issues),
please check that it has not already been reported by searching for some related keywords. Please
also check [`tui-rs` issues](https://github.com/fdehau/tui-rs/issues/) and link any related issues
found.
## Pull requests
All contributions are obviously welcome. Please include as many details as possible in your PR
description to help the reviewer (follow the provided template). Make sure to highlight changes
which may need additional attention, or you are uncertain about. Any idea with a large scale impact
on the crate or its users should ideally be discussed in a "Feature Request" issue beforehand.
### Keep PRs small, intentional, and focused
Try to do one pull request per change. The time taken to review a PR grows exponential with the size
of the change. Small focused PRs will generally be much more faster to review. PRs that include both
refactoring (or reformatting) with actual changes are more difficult to review as every line of the
change becomes a place where a bug may have been introduced. Consider splitting refactoring /
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
history of the project. Please search, read, link, and summarize any relevant
[issues](https://github.com/fdehau/tui-rs/issues/),
[discussions](https://github.com/fdehau/tui-rs/discussions/) and [pull
requests](https://github.com/fdehau/tui-rs/pulls).
### Use conventional commits
We use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) and check for them as
a lint build step. To help adhere to the format, we recommend to install
[Commitizen](https://commitizen-tools.github.io/commitizen/). By using this tool you automatically
follow the configuration defined in [.cz.toml](.cz.toml). Your commit messages should have enough
information to help someone reading the [CHANGELOG](./CHANGELOG.md) understand what is new just from
the title. The summary helps expand on that to provide information that helps provide more context,
describes the nature of the problem that the commit is solving and any unintuitive effects of the
change. It's rare that code changes can easily communicate intent, so make sure this is clearly
documented.
### Run CI tests before pushing a PR
Running `cargo make ci` before pushing will perform the same checks that we do in the CI process.
It's not mandatory to do this before pushing, however it may save you time to do so instead of
waiting for GitHub to run the checks.
### Sign your commits
We use commit signature verification, which will block commits from being merged via the UI unless
they are signed. To set up your machine to sign commits, see [managing commit signature
verification](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification)
in GitHub docs.
## Implementation Guidelines
### Setup
Clone the repo and build it using [cargo-make](https://sagiegurari.github.io/cargo-make/)
Ratatui is an ordinary Rust project where common tasks are managed with
[cargo-make](https://github.com/sagiegurari/cargo-make/). It wraps common `cargo` commands with sane
defaults depending on your platform of choice. Building the project should be as easy as running
`cargo make build`.
```shell
git clone https://github.com/ratatui/ratatui.git
cd ratatui
cargo make build
```
### Tests
The [test coverage](https://app.codecov.io/gh/ratatui/ratatui) of the crate is reasonably
good, but this can always be improved. Focus on keeping the tests simple and obvious and write unit
tests for all new or modified code. Beside the usual doc and unit tests, one of the most valuable
test you can write for Ratatui is a test against the `TestBackend`. It allows you to assert the
content of the output buffer that would have been flushed to the terminal after a given draw call.
See `widgets_block_renders` in [tests/widgets_block.rs](./tests/widget_block.rs) for an example.
When writing tests, generally prefer to write unit tests and doc tests directly in the code file
being tested rather than integration tests in the `tests/` folder.
If an area that you're making a change in is not tested, write tests to characterize the existing
behavior before changing it. This helps ensure that we don't introduce bugs to existing software
using Ratatui (and helps make it easy to migrate apps still using `tui-rs`).
For coverage, we have two [bacon](https://dystroy.org/bacon/) jobs (one for all tests, and one for
unit tests, keyboard shortcuts `v` and `u` respectively) that run
[cargo-llvm-cov](https://github.com/taiki-e/cargo-llvm-cov) to report the coverage. Several plugins
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 [VS Code 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]``
### Deprecation notice
We generally want to wait at least two versions before removing deprecated items, so users have
time to update. However, if a deprecation is blocking for us to implement a new feature we may
*consider* removing it in a one version notice.
### 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
may be specific cases that this becomes necessary in order to avoid slowness. Please see [this
discussion](https://github.com/ratatui/ratatui/discussions/66) for more about the decision.
## Continuous Integration
We use GitHub Actions for the CI where we perform the following checks:
- The code should compile on `stable` and the Minimum Supported Rust Version (MSRV).
- The tests (docs, lib, tests and examples) should pass.
- The code should conform to the default format enforced by `rustfmt`.
- The code should not contain common style issues `clippy`.
You can also check most of those things yourself locally using `cargo make ci` which will offer you
a shorter feedback loop than pushing to github.
## Relationship with `tui-rs`
This project was forked from [`tui-rs`](https://github.com/fdehau/tui-rs/) in February 2023, with the
[blessing of the original author](https://github.com/fdehau/tui-rs/issues/654), Florian Dehau
([@fdehau](https://github.com/fdehau)).
The original repository contains all the issues, PRs, and discussion that were raised originally, and
it is useful to refer to when contributing code, documentation, or issues with Ratatui.
We imported all the PRs from the original repository, implemented many of the smaller ones, and
made notes on the leftovers. These are marked as draft PRs and labelled as [imported from
tui](https://github.com/ratatui/ratatui/pulls?q=is%3Apr+is%3Aopen+label%3A%22imported+from+tui%22).
We have documented the current state of those PRs, and anyone is welcome to pick them up and
continue the work on them.
We have not imported all issues opened on the previous repository. For that reason, anyone wanting
to **work on or discuss** an issue will have to follow the following workflow:
- Recreate the issue
- Start by referencing the **original issue**: ```Referencing issue #[<issue number>](<original
issue link>)```
- Then, paste the original issues **opening** text
You can then resume the conversation by replying to this new issue you have created.

View File

@@ -1,127 +1,380 @@
[package]
name = "tui"
version = "0.9.2"
authors = ["Florian Dehau <work@fdehau.com>"]
description = """
A library to build rich terminal user interfaces or dashboards
"""
documentation = "https://docs.rs/tui/0.9.2/tui/"
name = "ratatui"
version = "0.28.1" # 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/"
repository = "https://github.com/ratatui/ratatui"
homepage = "https://ratatui.rs"
keywords = ["tui", "terminal", "dashboard"]
repository = "https://github.com/fdehau/tui-rs"
categories = ["command-line-interface"]
readme = "README.md"
license = "MIT"
exclude = ["assets/*", ".github"]
autoexamples = true
edition = "2018"
[badges]
[features]
default = ["termion"]
curses = ["easycurses", "pancurses"]
exclude = [
"assets/*",
".github",
"Makefile.toml",
"CONTRIBUTING.md",
"*.log",
"tags",
]
edition = "2021"
rust-version = "1.74.0"
[dependencies]
bitflags = "1.0"
bitflags = "2.3"
cassowary = "0.3"
itertools = "0.9"
either = "1.5"
unicode-segmentation = "1.2"
unicode-width = "0.1"
termion = { version = "1.5", optional = true }
rustbox = { version = "0.11", optional = true }
crossterm = { version = "0.17", optional = true }
easycurses = { version = "0.12.2", optional = true }
pancurses = { version = "0.16.1", optional = true, features = ["win32a"] }
compact_str = "0.8.0"
crossterm = { version = "0.28.1", optional = true }
document-features = { version = "0.2.7", optional = true }
instability = "0.3.1"
itertools = "0.13"
lru = "0.12.0"
paste = "1.0.2"
palette = { version = "0.7.6", optional = true }
serde = { version = "1", optional = true, features = ["derive"] }
strum = { version = "0.26.3", features = ["derive"] }
termwiz = { version = "0.22.0", optional = true }
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
unicode-segmentation = "1.10"
unicode-truncate = "1"
unicode-width = "0.1.13"
[target.'cfg(not(windows))'.dependencies]
# termion is not supported on Windows
termion = { version = "4.0.0", optional = true }
[dev-dependencies]
rand = "0.7"
argh = "0.1"
argh = "0.1.12"
color-eyre = "0.6.2"
criterion = { version = "0.5.1", features = ["html_reports"] }
crossterm = { version = "0.28.1", features = ["event-stream"] }
fakeit = "1.1"
font8x8 = "0.3.1"
futures = "0.3.30"
indoc = "2"
octocrab = "0.39.0"
pretty_assertions = "1.4.0"
rand = "0.8.5"
rand_chacha = "0.3.1"
rstest = "0.22.0"
serde_json = "1.0.109"
tokio = { version = "1.39.2", features = [
"rt",
"macros",
"time",
"rt-multi-thread",
] }
tracing = "0.1.40"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
[lints.rust]
unsafe_code = "forbid"
[lints.clippy]
cargo = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
cast_possible_truncation = "allow"
cast_possible_wrap = "allow"
cast_precision_loss = "allow"
cast_sign_loss = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
module_name_repetitions = "allow"
must_use_candidate = "allow"
# we often split up a module into multiple files with the main type in a file named after the
# module, so we want to allow this pattern
module_inception = "allow"
# nursery or restricted
as_underscore = "warn"
deref_by_slicing = "warn"
else_if_without_else = "warn"
empty_line_after_doc_comments = "warn"
equatable_if_let = "warn"
fn_to_numeric_cast_any = "warn"
format_push_string = "warn"
map_err_ignore = "warn"
missing_const_for_fn = "warn"
mixed_read_write_in_expression = "warn"
mod_module_files = "warn"
needless_pass_by_ref_mut = "warn"
needless_raw_strings = "warn"
or_fun_call = "warn"
redundant_type_annotations = "warn"
rest_pat_in_fully_bound_structs = "warn"
string_lit_chars_any = "warn"
string_slice = "warn"
string_to_string = "warn"
unnecessary_self_imports = "warn"
use_self = "warn"
[features]
#! 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::CrosstermBackend) backend and adds a dependency on [`crossterm`].
crossterm = ["dep:crossterm"]
## enables the [`TermionBackend`](backend::TermionBackend) backend and adds a dependency on [`termion`].
termion = ["dep:termion"]
## enables the [`TermwizBackend`](backend::TermwizBackend) backend and adds a dependency on [`termwiz`].
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.
serde = ["dep:serde", "bitflags/serde", "compact_str/serde"]
## enables the [`border!`] macro.
macros = []
## enables conversions from colors in the [`palette`] crate to [`Color`](crate::style::Color).
palette = ["dep:palette"]
## enables all widgets.
all-widgets = ["widget-calendar"]
#! Widgets that add dependencies are gated behind feature flags to prevent unused transitive
#! dependencies. The available features are:
## enables the [`calendar`](widgets::calendar) widget module and adds a dependency on [`time`].
widget-calendar = ["dep:time"]
#! The following optional features are only available for some backends:
## enables the backend code that sets the underline color.
## Underline color is only supported by the [`CrosstermBackend`](backend::CrosstermBackend) backend,
## and is not supported on Windows 7.
underline-color = ["dep:crossterm"]
#! The following features are unstable and may change in the future:
## Enable all unstable features.
unstable = [
"unstable-rendered-line-info",
"unstable-widget-ref",
"unstable-backend-writer",
]
## Enables the [`Paragraph::line_count`](widgets::Paragraph::line_count)
## [`Paragraph::line_width`](widgets::Paragraph::line_width) methods
## which are experimental and may change in the future.
## See [Issue 293](https://github.com/ratatui/ratatui/issues/293) for more details.
unstable-rendered-line-info = []
## Enables the [`WidgetRef`](widgets::WidgetRef) and [`StatefulWidgetRef`](widgets::StatefulWidgetRef) traits which are experimental and may change in
## the future.
unstable-widget-ref = []
## Enables getting access to backends' writers.
unstable-backend-writer = []
[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"]
# Improve benchmark consistency
[profile.bench]
codegen-units = 1
lto = true
[lib]
bench = false
[[bench]]
name = "main"
harness = false
[[example]]
name = "canvas"
path = "examples/canvas.rs"
required-features = ["termion"]
[[example]]
name = "user_input"
path = "examples/user_input.rs"
required-features = ["termion"]
[[example]]
name = "gauge"
path = "examples/gauge.rs"
required-features = ["termion"]
name = "async"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "barchart"
path = "examples/barchart.rs"
required-features = ["termion"]
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "chart"
path = "examples/chart.rs"
required-features = ["termion"]
[[example]]
name = "paragraph"
path = "examples/paragraph.rs"
required-features = ["termion"]
[[example]]
name = "list"
path = "examples/list.rs"
required-features = ["termion"]
[[example]]
name = "table"
path = "examples/table.rs"
required-features = ["termion"]
[[example]]
name = "tabs"
path = "examples/tabs.rs"
required-features = ["termion"]
[[example]]
name = "custom_widget"
path = "examples/custom_widget.rs"
required-features = ["termion"]
[[example]]
name = "layout"
path = "examples/layout.rs"
required-features = ["termion"]
[[example]]
name = "popup"
path = "examples/popup.rs"
required-features = ["termion"]
name = "barchart-grouped"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "block"
path = "examples/block.rs"
required-features = ["termion"]
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "calendar"
required-features = ["crossterm", "widget-calendar"]
doc-scrape-examples = true
[[example]]
name = "canvas"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "chart"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "colors"
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", "palette"]
doc-scrape-examples = true
[[example]]
name = "constraint-explorer"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "constraints"
required-features = ["crossterm"]
doc-scrape-examples = false
[[example]]
name = "custom_widget"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
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", "palette", "widget-calendar"]
doc-scrape-examples = true
[[example]]
name = "docsrs"
required-features = ["crossterm"]
doc-scrape-examples = false
[[example]]
name = "flex"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "gauge"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "hello_world"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "inline"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "layout"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "line_gauge"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "hyperlink"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "list"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "minimal"
required-features = ["crossterm"]
# prefer to show the more featureful examples in the docs
doc-scrape-examples = false
[[example]]
name = "modifiers"
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 = "panic"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "paragraph"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
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"]
doc-scrape-examples = true
[[example]]
name = "sparkline"
path = "examples/sparkline.rs"
required-features = ["termion"]
[[example]]
name = "termion_demo"
path = "examples/termion_demo.rs"
required-features = ["termion"]
[[example]]
name = "rustbox_demo"
path = "examples/rustbox_demo.rs"
required-features = ["rustbox"]
[[example]]
name = "crossterm_demo"
path = "examples/crossterm_demo.rs"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "curses_demo"
path = "examples/curses_demo.rs"
required-features = ["curses"]
name = "table"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "tabs"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "tracing"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "user_input"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "widget_impl"
required-features = ["crossterm", "unstable-widget-ref"]
doc-scrape-examples = true
[[test]]
name = "state_serde"
required-features = ["serde"]

7
FUNDING.json Normal file
View File

@@ -0,0 +1,7 @@
{
"drips": {
"ethereum": {
"ownedBy": "0x6053C8984f4F214Ad12c4653F28514E1E09213B5"
}
}
}

View File

@@ -1,6 +1,7 @@
The MIT License (MIT)
Copyright (c) 2016 Florian Dehau
Copyright (c) 2016-2022 Florian Dehau
Copyright (c) 2023-2024 The Ratatui Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

15
MAINTAINERS.md Normal file
View File

@@ -0,0 +1,15 @@
# Maintainers
This file documents current and past maintainers.
- [orhun](https://github.com/orhun)
- [joshka](https://github.com/joshka)
- [kdheepak](https://github.com/kdheepak)
- [Valentin271](https://github.com/Valentin271)
## Past Maintainers
- [fdehau](https://github.com/fdehau)
- [mindoodoo](https://github.com/mindoodoo)
- [sayanarijit](https://github.com/sayanarijit)
- [EdJoPaTo](https://github.com/EdJoPaTo)

113
Makefile
View File

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

154
Makefile.toml Normal file
View File

@@ -0,0 +1,154 @@
# configuration for https://github.com/sagiegurari/cargo-make
[config]
skip_core_tasks = true
[env]
# all features except the backend ones
NON_BACKEND_FEATURES = "all-widgets,macros,serde"
[tasks.default]
alias = "ci"
[tasks.ci]
description = "Run continuous integration tasks"
dependencies = ["lint", "clippy", "check", "test"]
[tasks.lint]
description = "Lint code style (formatting, typos, docs, markdown)"
dependencies = ["lint-format", "lint-typos", "lint-docs"]
[tasks.lint-format]
description = "Lint code formatting"
toolchain = "nightly"
command = "cargo"
args = ["fmt", "--all", "--check"]
[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",
"--all-features",
"--",
"-Zunstable-options",
"--check",
"-Dwarnings",
]
[tasks.lint-markdown]
description = "Check markdown files for errors and warnings"
command = "markdownlint-cli2"
args = ["**/*.md", "!target"]
[tasks.check]
description = "Check code for errors and warnings"
command = "cargo"
args = ["check", "--all-targets", "--all-features"]
[tasks.build]
description = "Compile the project"
command = "cargo"
args = ["build", "--all-targets", "--all-features"]
[tasks.clippy]
description = "Run Clippy for linting"
command = "cargo"
args = [
"clippy",
"--all-targets",
"--all-features",
"--tests",
"--benches",
"--",
"-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"
run_task = { name = ["test-lib", "test-doc"] }
[tasks.test-lib]
description = "Run default tests"
dependencies = ["install-nextest"]
command = "cargo"
args = ["nextest", "run", "--all-targets", "--all-features"]
[tasks.test-doc]
description = "Run documentation tests"
command = "cargo"
args = ["test", "--doc", "--all-features"]
[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 = [
"nextest",
"run",
"--all-targets",
"--no-default-features",
"--features",
"${NON_BACKEND_FEATURES},${@}",
]
[tasks.coverage]
description = "Generate code coverage report"
command = "cargo"
args = [
"llvm-cov",
"--lcov",
"--output-path",
"target/lcov.info",
"--all-features",
]
[tasks.run-example]
private = true
condition = { env_set = ["TUI_EXAMPLE_NAME"] }
command = "cargo"
args = [
"run",
"--release",
"--example",
"${TUI_EXAMPLE_NAME}",
"--features",
"all-widgets",
]
[tasks.build-examples]
description = "Compile project examples"
command = "cargo"
args = ["build", "--examples", "--release", "--features", "all-widgets"]
[tasks.run-examples]
description = "Run project examples"
dependencies = ["build-examples"]
script = '''
#!@duckscript
files = glob_array ./examples/*.rs
for file in ${files}
name = basename ${file}
name = substring ${name} -3
set_env TUI_EXAMPLE_NAME ${name}
cm_run_task run-example
end
'''

473
README.md
View File

@@ -1,110 +1,433 @@
# tui-rs
<details>
<summary>Table of Contents</summary>
[![Build Status](https://github.com/fdehau/tui-rs/workflows/CI/badge.svg)](https://github.com/fdehau/tui-rs/actions?query=workflow%3ACI+)
[![Crate Status](https://img.shields.io/crates/v/tui.svg)](https://crates.io/crates/tui)
[![Docs Status](https://docs.rs/tui/badge.svg)](https://docs.rs/crate/tui/)
- [Ratatui](#ratatui)
- [Installation](#installation)
- [Introduction](#introduction)
- [Other documentation](#other-documentation)
- [Quickstart](#quickstart)
- [Initialize and restore the terminal](#initialize-and-restore-the-terminal)
- [Drawing the UI](#drawing-the-ui)
- [Handling events](#handling-events)
- [Example](#example)
- [Layout](#layout)
- [Text and styling](#text-and-styling)
- [Status of this fork](#status-of-this-fork)
- [Widgets](#widgets)
- [Built in](#built-in)
- [Third-party libraries, bootstrapping templates and widgets](#third-party-libraries-bootstrapping-templates-and-widgets)
- [Apps](#apps)
- [Alternatives](#alternatives)
- [Acknowledgments](#acknowledgments)
- [License](#license)
<img src="./assets/demo.gif" alt="Demo cast under Linux Termite with Inconsolata font 12pt">
</details>
`tui-rs` is a [Rust](https://www.rust-lang.org) library to build rich terminal
user interfaces and dashboards. It is heavily inspired by the `Javascript`
library [blessed-contrib](https://github.com/yaronn/blessed-contrib) and the
`Go` library [termui](https://github.com/gizak/termui).
<!-- cargo-rdme start -->
The library itself supports four different backends to draw to the terminal. You
can either choose from:
![Demo](https://github.com/ratatui/ratatui/blob/87ae72dbc756067c97f6400d3e2a58eeb383776e/examples/demo2-destroy.gif?raw=true)
- [termion](https://github.com/ticki/termion)
- [rustbox](https://github.com/gchp/rustbox)
- [crossterm](https://github.com/crossterm-rs/crossterm)
- [pancurses](https://github.com/ihalila/pancurses)
<div align="center">
However, some features may only be available in one of the four.
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![Deps.rs
Badge]][Deps.rs]<br> [![Codecov Badge]][Codecov] [![License Badge]](./LICENSE) [![Sponsors
Badge]][GitHub Sponsors]<br> [![Discord Badge]][Discord Server] [![Matrix Badge]][Matrix]
[![Forum Badge]][Forum]<br>
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.
[Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br>
[Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request]
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.
</div>
### [Documentation](https://docs.rs/tui)
# Ratatui
### Demo
[Ratatui][Ratatui Website] 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.
The demo shown in the gif can be run with all available backends
(`exmples/*_demo.rs` files). For example to see the `termion` version one could
run:
## Installation
```
cargo run --example termion_demo --release -- --tick-rate 200
Add `ratatui` as a dependency to your cargo.toml:
```shell
cargo add ratatui
```
where `tick-rate` is the UI refresh rate in ms.
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]).
The UI code is in [examples/demo/ui.rs](examples/demo/ui.rs) while the
application state is in [examples/demo/app.rs](examples/demo/app.rs).
## Introduction
Beware that the `termion_demo` only works on Unix platforms. If you are a Windows user,
you can see the same demo using the `crossterm` backend with the following command:
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.
```
cargo run --example crossterm_demo --no-default-features --features="crossterm" --release -- --tick-rate 200
You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to
terminal user interfaces and showcases the features of Ratatui, along with a hello world demo.
## Other documentation
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
- [Ratatui Forum][Forum] - a place to ask questions and discuss the library
- [API Docs] - the full API documentation for the library on docs.rs.
- [Examples] - a collection of examples that demonstrate how to use the library.
- [Contributing] - Please read this if you are interested in contributing to the project.
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
- [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
the [Examples] directory. 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 in the [templates]
repository.
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. After this closure returns, a diff is performed and
only the changes are drawn to the terminal. 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
use std::io::{self, stdout};
use ratatui::{
backend::CrosstermBackend,
crossterm::{
event::{self, Event, KeyCode},
terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
},
ExecutableCommand,
},
widgets::{Block, Paragraph},
Frame, Terminal,
};
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 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::bordered().title("Greeting")),
frame.area(),
);
}
```
If the user interface contains glyphs that are not displayed correctly by your terminal, you may want to run
the demo without those symbols:
Running this example produces the following output:
```
cargo run --example crossterm_demo --no-default-features --features="crossterm" --release -- --tick-rate 200 --enhanced-graphics false
![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::{
layout::{Constraint, Layout},
widgets::Block,
Frame,
};
fn ui(frame: &mut Frame) {
let [title_area, main_area, status_area] = Layout::vertical([
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
])
.areas(frame.area());
let [left_area, right_area] =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.areas(main_area);
frame.render_widget(Block::bordered().title("Title Bar"), title_area);
frame.render_widget(Block::bordered().title("Status Bar"), status_area);
frame.render_widget(Block::bordered().title("Left"), left_area);
frame.render_widget(Block::bordered().title("Right"), right_area);
}
```
### Widgets
Running this example produces the following output:
The library comes with the following list of widgets:
![docsrs-layout]
* [Block](examples/block.rs)
* [Gauge](examples/gauge.rs)
* [Sparkline](examples/sparkline.rs)
* [Chart](examples/chart.rs)
* [BarChart](examples/barchart.rs)
* [List](examples/list.rs)
* [Table](examples/table.rs)
* [Paragraph](examples/paragraph.rs)
* [Canvas (with line, point cloud, map)](examples/canvas.rs)
* [Tabs](examples/tabs.rs)
## Text and styling
Click on each item to see the source of the example. Run the examples with with
cargo (e.g. to run the demo `cargo run --example demo`), and quit by pressing `q`.
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.
You can run all examples by running `make run-examples`.
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.
### Third-party widgets
```rust
use ratatui::{
layout::{Constraint, Layout},
style::{Color, Modifier, Style, Stylize},
text::{Line, Span},
widgets::{Block, Paragraph},
Frame,
};
* [tui-logger](https://github.com/gin66/tui-logger)
fn ui(frame: &mut Frame) {
let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.area());
### Apps using tui
let line = Line::from(vec![
Span::raw("Hello "),
Span::styled(
"World",
Style::new()
.fg(Color::Green)
.bg(Color::White)
.add_modifier(Modifier::BOLD),
),
"!".red().on_light_yellow().italic(),
]);
frame.render_widget(line, areas[0]);
* [spotify-tui](https://github.com/Rigellute/spotify-tui)
* [bandwhich](https://github.com/imsnif/bandwhich)
* [kmon](https://github.com/orhun/kmon)
* [ytop](https://github.com/cjbassi/ytop)
* [zenith](https://github.com/bvaisvil/zenith)
* [bottom](https://github.com/ClementTsang/bottom)
* [oha](https://github.com/hatoo/oha)
* [gitui](https://github.com/extrawurst/gitui)
* [rust-sadari-cli](https://github.com/24seconds/rust-sadari-cli)
* [desed](https://github.com/SoptikHa2/desed)
// using the short-hand syntax and implicit conversions
let paragraph = Paragraph::new("Hello World!".red().on_white().bold());
frame.render_widget(paragraph, areas[1]);
### Alternatives
// style the whole widget instead of just the text
let paragraph = Paragraph::new("Hello World!").style(Style::new().red().on_white());
frame.render_widget(paragraph, areas[2]);
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an
alternative solution to build text user interfaces in Rust.
// use the simpler short-hand syntax
let paragraph = Paragraph::new("Hello World!").blue().on_yellow();
frame.render_widget(paragraph, 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/
[templates]: https://github.com/ratatui/templates/
[Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md
[Report a bug]: https://github.com/ratatui/ratatui/issues/new?labels=bug&projects=&template=bug_report.md
[Request a Feature]: https://github.com/ratatui/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md
[Create a Pull Request]: https://github.com/ratatui/ratatui/compare
[git-cliff]: https://git-cliff.org
[Conventional Commits]: https://www.conventionalcommits.org
[API Docs]: https://docs.rs/ratatui
[Changelog]: https://github.com/ratatui/ratatui/blob/main/CHANGELOG.md
[Contributing]: https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md
[Breaking Changes]: https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md
[FOSDEM 2024 talk]: https://www.youtube.com/watch?v=NU0q6NOLJ20
[docsrs-hello]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
[docsrs-layout]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
[docsrs-styling]: https://github.com/ratatui/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
[Crate]: https://crates.io/crates/ratatui
[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
[GitHub Sponsors]: https://github.com/sponsors/ratatui
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square&logoColor=E05D44&color=E05D44
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square&color=1370D3
[CI Badge]: https://img.shields.io/github/actions/workflow/status/ratatui/ratatui/ci.yml?style=flat-square&logo=github
[CI Workflow]: https://github.com/ratatui/ratatui/actions/workflows/ci.yml
[Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
[Codecov]: https://app.codecov.io/gh/ratatui/ratatui
[Deps.rs Badge]: https://deps.rs/repo/github/ratatui/ratatui/status.svg?style=flat-square
[Deps.rs]: https://deps.rs/repo/github/ratatui/ratatui
[Discord Badge]: https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3
[Discord Server]: https://discord.gg/pMCEU9hNEj
[Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square&logoColor=E05D44
[Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
[Forum Badge]: https://img.shields.io/discourse/likes?server=https%3A%2F%2Fforum.ratatui.rs&style=flat-square&logo=discourse&label=forum&color=C43AC3
[Forum]: https://forum.ratatui.rs
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui?logo=github&style=flat-square&color=1370D3
<!-- cargo-rdme end -->
## Status of this fork
In response to the original maintainer [**Florian Dehau**](https://github.com/fdehau)'s issue
regarding the [future of `tui-rs`](https://github.com/fdehau/tui-rs/issues/654), several members of
the community forked the project and created this crate. We look forward to continuing the work
started by Florian 🚀
In order to organize ourselves, we currently use a [Discord server](https://discord.gg/pMCEU9hNEj),
feel free to join and come chat! There is also a [Matrix](https://matrix.org/) bridge available at
[#ratatui:matrix.org](https://matrix.to/#/#ratatui:matrix.org).
While we do utilize Discord for coordinating, it's not essential for contributing. We have recently
launched the [Ratatui Forum][Forum], and our primary open-source workflow is centered around GitHub.
For bugs and features, we rely on GitHub. Please [Report a bug], [Request a Feature] or [Create a
Pull Request].
Please make sure you read the updated [contributing](./CONTRIBUTING.md) guidelines, especially if
you are interested in working on a PR or issue opened in the previous repository.
## Widgets
### Built in
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
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)
Each widget has an associated example which can be found in the [Examples] folder. Run each example
with cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by pressing `q`.
You can also run all examples by running `cargo make run-examples` (requires `cargo-make` that can
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
`ratatui::text::Text`
- [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
`ratatui::style::Color`
- [templates](https://github.com/ratatui/templates) — Starter templates for
bootstrapping a Rust TUI application with Ratatui & 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
with a React/Elm inspired approach
- [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
structures.
- [tui-windows](https://github.com/markatk/tui-windows-rs) — Tui-rs abstraction to handle multiple
windows and their rendering
- [tui-textarea](https://github.com/rhysd/tui-textarea) — Simple yet powerful multi-line text editor
widget supporting several key shortcuts, undo/redo, text search, etc.
- [tui-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
that enables the rendering of terminal applications as ratatui widgets.
## Apps
Check out [awesome-ratatui](https://github.com/ratatui/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.
## Acknowledgments
Special thanks to [**Pavel Fomchenkov**](https://github.com/nawok) for his work in designing **an
awesome logo** for the ratatui project and ratatui organization.
## License
[MIT](LICENSE)
[MIT](./LICENSE)

51
RELEASE.md Normal file
View File

@@ -0,0 +1,51 @@
# Creating a Release
Our release strategy is:
> Release major versions with detailed summaries when necessary, while releasing minor versions
> weekly or as needed without extensive announcements.
>
> Versioning scheme being `0.x.y`, where `x` is the major version and `y` is the minor version.
[crates.io](https://crates.io/crates/ratatui) releases are automated via [GitHub
actions](.github/workflows/cd.yml) and triggered by pushing a tag.
1. Record a new demo gif if necessary. The preferred tool for this is
[vhs](https://github.com/charmbracelet/vhs) (installation instructions in README).
```shell
cargo build --example demo2
vhs examples/demo2.tape
```
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/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. 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[0.x.y]`
1. Push the tag: `git push --tags`
1. Wait for [Continuous Deployment](https://github.com/ratatui/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/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/ratatui/issues/147) and
[#359](https://github.com/ratatui/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/ratatui/security/advisories/new>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

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

112
bacon.toml Normal file
View File

@@ -0,0 +1,112 @@
# This is a configuration file for the bacon tool
#
# Bacon repository: https://github.com/Canop/bacon
# Complete help on configuration: https://dystroy.org/bacon/config/
# You can also check bacon's own bacon.toml file
# as an example: https://github.com/Canop/bacon/blob/main/bacon.toml
default_job = "check"
[jobs.check]
command = ["cargo", "check", "--all-features", "--color", "always"]
need_stdout = false
[jobs.check-all]
command = ["cargo", "check", "--all-targets", "--all-features", "--color", "always"]
need_stdout = false
[jobs.check-crossterm]
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "crossterm"]
need_stdout = false
[jobs.check-termion]
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termion"]
need_stdout = false
[jobs.check-termwiz]
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termwiz"]
need_stdout = false
[jobs.clippy]
command = [
"cargo", "clippy",
"--all-targets",
"--color", "always",
]
need_stdout = false
[jobs.test]
command = [
"cargo", "test",
"--all-features",
"--color", "always",
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
]
need_stdout = true
[jobs.test-unit]
command = [
"cargo", "test",
"--lib",
"--all-features",
"--color", "always",
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
]
need_stdout = true
[jobs.doc]
command = [
"cargo", "+nightly", "doc",
"-Zunstable-options", "-Zrustdoc-scrape-examples",
"--all-features",
"--color", "always",
"--no-deps",
]
env.RUSTDOCFLAGS = "--cfg docsrs"
need_stdout = false
# If the doc compiles, then it opens in your browser and bacon switches
# to the previous job
[jobs.doc-open]
command = [
"cargo", "+nightly", "doc",
"-Zunstable-options", "-Zrustdoc-scrape-examples",
"--all-features",
"--color", "always",
"--no-deps",
"--open",
]
env.RUSTDOCFLAGS = "--cfg docsrs"
need_stdout = false
on_success = "job:doc" # so that we don't open the browser at each change
[jobs.coverage]
command = [
"cargo", "llvm-cov",
"--lcov", "--output-path", "target/lcov.info",
"--all-features",
"--color", "always",
]
[jobs.coverage-unit-tests-only]
command = [
"cargo", "llvm-cov",
"--lcov", "--output-path", "target/lcov.info",
"--lib",
"--all-features",
"--color", "always",
]
# You may define here keybindings that would be specific to
# a project, for example a shortcut to launch a specific job.
# Shortcuts to internal functions (scrolling, toggling, etc.)
# should go in your personal global prefs.toml file instead.
[keybindings]
# alt-m = "job:my-job"
ctrl-c = "job:check-crossterm"
ctrl-t = "job:check-termion"
ctrl-w = "job:check-termwiz"
v = "job:coverage"
ctrl-v = "job:coverage-unit-tests-only"
u = "job:test-unit"
n = "job:nextest"

22
benches/main.rs Normal file
View File

@@ -0,0 +1,22 @@
pub mod main {
pub mod barchart;
pub mod block;
pub mod buffer;
pub mod line;
pub mod list;
pub mod paragraph;
pub mod rect;
pub mod sparkline;
}
pub use main::*;
criterion::criterion_main!(
barchart::benches,
block::benches,
buffer::benches,
line::benches,
list::benches,
paragraph::benches,
rect::benches,
sparkline::benches
);

72
benches/main/barchart.rs Normal file
View File

@@ -0,0 +1,72 @@
use criterion::{criterion_group, Bencher, BenchmarkId, Criterion};
use rand::Rng;
use ratatui::{
buffer::Buffer,
layout::Rect,
prelude::Direction,
widgets::{Bar, BarChart, BarGroup, Widget},
};
/// Benchmark for rendering a barchart.
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/ratatui/pull/377.
bencher.iter_batched(
|| barchart.clone(),
|bench_barchart| {
bench_barchart.render(buffer.area, &mut buffer);
},
criterion::BatchSize::LargeInput,
);
}
criterion_group!(benches, barchart);

55
benches/main/block.rs Normal file
View File

@@ -0,0 +1,55 @@
use criterion::{criterion_group, BatchSize, Bencher, Criterion};
use ratatui::{
buffer::Buffer,
layout::{Alignment, Rect},
text::Line,
widgets::{Block, Padding, Widget},
};
/// Benchmark for rendering a block.
fn block(c: &mut Criterion) {
let mut group = c.benchmark_group("block");
for (width, height) in [
(100, 50), // vertically split screen
(200, 50), // 1080p fullscreen with medium font
(256, 256), // Max sized area
] {
let buffer_size = Rect::new(0, 0, width, height);
// Render an empty block
group.bench_with_input(
format!("render_empty/{width}x{height}"),
&Block::new(),
|b, block| render(b, block, buffer_size),
);
// Render with all features
group.bench_with_input(
format!("render_all_feature/{width}x{height}"),
&Block::bordered()
.padding(Padding::new(5, 5, 2, 2))
.title("test title")
.title_bottom(Line::from("bottom left title").alignment(Alignment::Right)),
|b, block| render(b, block, buffer_size),
);
}
group.finish();
}
/// render the block into a buffer of the given `size`
fn render(bencher: &mut Bencher, block: &Block, size: Rect) {
let mut buffer = Buffer::empty(size);
// We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui/ratatui/pull/377.
bencher.iter_batched(
|| block.to_owned(),
|bench_block| {
bench_block.render(buffer.area, &mut buffer);
},
BatchSize::SmallInput,
);
}
criterion_group!(benches, block);

65
benches/main/buffer.rs Normal file
View File

@@ -0,0 +1,65 @@
use criterion::{black_box, BenchmarkId, Criterion};
use ratatui::{
buffer::{Buffer, Cell},
layout::Rect,
text::Line,
};
criterion::criterion_group!(benches, empty, filled, with_lines);
const fn rect(size: u16) -> Rect {
Rect {
x: 0,
y: 0,
width: size,
height: size,
}
}
fn empty(c: &mut Criterion) {
let mut group = c.benchmark_group("buffer/empty");
for size in [16, 64, 255] {
let area = rect(size);
group.bench_with_input(BenchmarkId::from_parameter(size), &area, |b, &area| {
b.iter(|| {
let _buffer = Buffer::empty(black_box(area));
});
});
}
group.finish();
}
/// This likely should have the same performance as `empty`, but it's here for completeness
/// and to catch any potential performance regressions.
fn filled(c: &mut Criterion) {
let mut group = c.benchmark_group("buffer/filled");
for size in [16, 64, 255] {
let area = rect(size);
let cell = Cell::new("AAAA"); // simulate a multi-byte character
group.bench_with_input(
BenchmarkId::from_parameter(size),
&(area, cell),
|b, (area, cell)| {
b.iter(|| {
let _buffer = Buffer::filled(black_box(*area), cell.clone());
});
},
);
}
group.finish();
}
fn with_lines(c: &mut Criterion) {
let mut group = c.benchmark_group("buffer/with_lines");
for size in [16, 64, 255] {
let word_count = 50;
let lines = fakeit::words::sentence(word_count);
let lines = lines.lines().map(Line::from);
group.bench_with_input(BenchmarkId::from_parameter(size), &lines, |b, lines| {
b.iter(|| {
let _buffer = Buffer::with_lines(black_box(lines.clone()));
});
});
}
group.finish();
}

38
benches/main/line.rs Normal file
View File

@@ -0,0 +1,38 @@
use std::hint::black_box;
use criterion::{criterion_group, Criterion};
use ratatui::{
buffer::Buffer,
layout::{Alignment, Rect},
style::Stylize,
text::Line,
widgets::Widget,
};
fn line_render(criterion: &mut Criterion) {
for alignment in [Alignment::Left, Alignment::Center, Alignment::Right] {
let mut group = criterion.benchmark_group(format!("line_render/{alignment}"));
group.sample_size(1000);
let line = &Line::from(vec![
"This".red(),
" ".green(),
"is".italic(),
" ".blue(),
"SPARTA!!".bold(),
])
.alignment(alignment);
for width in [0, 3, 4, 6, 7, 10, 42] {
let area = Rect::new(0, 0, width, 1);
group.bench_function(width.to_string(), |bencher| {
let mut buffer = Buffer::empty(area);
bencher.iter(|| black_box(line).render(area, &mut buffer));
});
}
group.finish();
}
}
criterion_group!(benches, line_render);

72
benches/main/list.rs Normal file
View File

@@ -0,0 +1,72 @@
use criterion::{criterion_group, BatchSize, Bencher, BenchmarkId, Criterion};
use ratatui::{
buffer::Buffer,
layout::Rect,
widgets::{List, ListItem, ListState, StatefulWidget, Widget},
};
/// Benchmark for rendering a list.
/// It only benchmarks the render with a different amount of items.
fn list(c: &mut Criterion) {
let mut group = c.benchmark_group("list");
for line_count in [64, 2048, 16384] {
let lines: Vec<ListItem> = (0..line_count)
.map(|_| ListItem::new(fakeit::words::sentence(10)))
.collect();
// Render default list
group.bench_with_input(
BenchmarkId::new("render", line_count),
&List::new(lines.clone()),
render,
);
// Render with an offset to the middle of the list and a selected item
group.bench_with_input(
BenchmarkId::new("render_scroll_half", line_count),
&List::new(lines.clone()).highlight_symbol(">>"),
|b, list| {
render_stateful(
b,
list,
ListState::default()
.with_offset(line_count / 2)
.with_selected(Some(line_count / 2)),
);
},
);
}
group.finish();
}
/// render the list into a common size buffer
fn render(bencher: &mut Bencher, list: &List) {
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/ratatui/pull/377.
bencher.iter_batched(
|| list.to_owned(),
|bench_list| {
Widget::render(bench_list, buffer.area, &mut buffer);
},
BatchSize::LargeInput,
);
}
/// render the list into a common size buffer with a state
fn render_stateful(bencher: &mut Bencher, list: &List, mut state: ListState) {
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/ratatui/pull/377.
bencher.iter_batched(
|| list.to_owned(),
|bench_list| {
StatefulWidget::render(bench_list, buffer.area, &mut buffer, &mut state);
},
BatchSize::LargeInput,
);
}
criterion_group!(benches, list);

94
benches/main/paragraph.rs Normal file
View File

@@ -0,0 +1,94 @@
use criterion::{black_box, criterion_group, BatchSize, Bencher, BenchmarkId, Criterion};
use ratatui::{
buffer::Buffer,
layout::Rect,
widgets::{Paragraph, Widget, Wrap},
};
/// because the scroll offset is a u16, the maximum number of lines that can be scrolled is 65535.
/// This is a limitation of the current implementation and may be fixed by changing the type of the
/// scroll offset to a u32.
const MAX_SCROLL_OFFSET: u16 = u16::MAX;
const NO_WRAP_WIDTH: u16 = 200;
const WRAP_WIDTH: u16 = 100;
/// Benchmark for rendering a paragraph with a given number of lines. The design of this benchmark
/// allows comparison of the performance of rendering a paragraph with different numbers of lines.
/// as well as comparing with the various settings on the scroll and wrap features.
fn paragraph(c: &mut Criterion) {
let mut group = c.benchmark_group("paragraph");
for line_count in [64, 2048, MAX_SCROLL_OFFSET] {
let lines = random_lines(line_count);
let lines = lines.as_str();
// benchmark that measures the overhead of creating a paragraph separately from rendering
group.bench_with_input(BenchmarkId::new("new", line_count), lines, |b, lines| {
b.iter(|| Paragraph::new(black_box(lines)));
});
// render the paragraph with no scroll
group.bench_with_input(
BenchmarkId::new("render", line_count),
&Paragraph::new(lines),
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
);
// scroll the paragraph by half the number of lines and render
group.bench_with_input(
BenchmarkId::new("render_scroll_half", line_count),
&Paragraph::new(lines).scroll((0, line_count / 2)),
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
);
// scroll the paragraph by the full number of lines and render
group.bench_with_input(
BenchmarkId::new("render_scroll_full", line_count),
&Paragraph::new(lines).scroll((0, line_count)),
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
);
// render the paragraph wrapped to 100 characters
group.bench_with_input(
BenchmarkId::new("render_wrap", line_count),
&Paragraph::new(lines).wrap(Wrap { trim: false }),
|bencher, paragraph| render(bencher, paragraph, WRAP_WIDTH),
);
// scroll the paragraph by the full number of lines and render wrapped to 100 characters
group.bench_with_input(
BenchmarkId::new("render_wrap_scroll_full", line_count),
&Paragraph::new(lines)
.wrap(Wrap { trim: false })
.scroll((0, line_count)),
|bencher, paragraph| render(bencher, paragraph, WRAP_WIDTH),
);
}
group.finish();
}
/// render the paragraph into a buffer with the given width
fn render(bencher: &mut Bencher, paragraph: &Paragraph, width: u16) {
let mut buffer = Buffer::empty(Rect::new(0, 0, width, 50));
// We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui/ratatui/pull/377.
bencher.iter_batched(
|| paragraph.to_owned(),
|bench_paragraph| {
bench_paragraph.render(buffer.area, &mut buffer);
},
BatchSize::LargeInput,
);
}
/// Create a string with the given number of lines filled with nonsense words
///
/// English language has about 5.1 average characters per word so including the space between words
/// this should emit around 200 characters per paragraph on average.
fn random_lines(count: u16) -> String {
let count = i64::from(count);
let sentence_count = 3;
let word_count = 11;
fakeit::words::paragraph(count, sentence_count, word_count, "\n".into())
}
criterion_group!(benches, paragraph);

24
benches/main/rect.rs Normal file
View File

@@ -0,0 +1,24 @@
use criterion::{black_box, criterion_group, BenchmarkId, Criterion};
use ratatui::layout::Rect;
fn rect_rows_benchmark(c: &mut Criterion) {
let rect_sizes = vec![
Rect::new(0, 0, 1, 16),
Rect::new(0, 0, 1, 1024),
Rect::new(0, 0, 1, 65535),
];
let mut group = c.benchmark_group("rect_rows");
for rect in rect_sizes {
group.bench_with_input(BenchmarkId::new("rows", rect.height), &rect, |b, rect| {
b.iter(|| {
for row in rect.rows() {
// Perform any necessary operations on each row
black_box(row);
}
});
});
}
group.finish();
}
criterion_group!(benches, rect_rows_benchmark);

44
benches/main/sparkline.rs Normal file
View File

@@ -0,0 +1,44 @@
use criterion::{criterion_group, Bencher, BenchmarkId, Criterion};
use rand::Rng;
use ratatui::{
buffer::Buffer,
layout::Rect,
widgets::{Sparkline, Widget},
};
/// Benchmark for rendering a sparkline.
fn sparkline(c: &mut Criterion) {
let mut group = c.benchmark_group("sparkline");
let mut rng = rand::thread_rng();
for data_count in [64, 256, 2048] {
let data: Vec<u64> = (0..data_count)
.map(|_| rng.gen_range(0..data_count))
.collect();
// Render a basic sparkline
group.bench_with_input(
BenchmarkId::new("render", data_count),
&Sparkline::default().data(&data),
render,
);
}
group.finish();
}
/// render the block into a buffer of the given `size`
fn render(bencher: &mut Bencher, sparkline: &Sparkline) {
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/ratatui/pull/377.
bencher.iter_batched(
|| sparkline.clone(),
|bench_sparkline| {
bench_sparkline.render(buffer.area, &mut buffer);
},
criterion::BatchSize::LargeInput,
);
}
criterion_group!(benches, sparkline);

148
cliff.toml Normal file
View File

@@ -0,0 +1,148 @@
# git-cliff ~ configuration file
# https://git-cliff.org/docs/configuration
[remote.github]
owner = "ratatui"
repo = "ratatui"
[changelog]
# changelog header
header = """
# Changelog
All notable changes to this project will be documented in this file.
<!-- ignore lint rules that are often triggered by content generated from commits / git-cliff -->
<!-- markdownlint-disable line-length no-bare-urls ul-style emphasis-style -->
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
# note that the - before / after the % controls whether whitespace is rendered between each line.
# Getting this right so that the markdown renders with the correct number of lines between headings
# code fences and list items is pretty finicky. Note also that the 4 backticks in the commit macro
# is intentional as this escapes any backticks in the commit body.
body = """
{%- if not version %}
## [unreleased]
{% else -%}
## [{{ version }}](https://github.com/ratatui/ratatui/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
{% endif -%}
{% macro commit(commit) -%}
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui/ratatui/commit/" ~ commit.id }}) \
*({{commit.scope | default(value = "uncategorized") | lower }})* {{ commit.message | upper_first | trim }}\
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif -%}\
{% if commit.github.pr_number %} in [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}){%- endif %}\
{%- if commit.breaking %} [**breaking**]{% endif %}
{%- if commit.body %}\n\n{{ commit.body | indent(prefix=" > ", first=true, blank=true) }}
{%- endif %}
{%- for footer in commit.footers %}\n
{%- if footer.token != "Signed-off-by" and footer.token != "Co-authored-by" %}
>
{{ footer.token | indent(prefix=" > ", first=true, blank=true) }}
{{- footer.separator }}
{{- footer.value| indent(prefix=" > ", first=false, blank=true) }}
{%- endif %}
{%- endfor %}
{% endmacro -%}
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits | filter(attribute="scope") | sort(attribute="scope") %}
{{ self::commit(commit=commit) }}
{%- endfor -%}
{% for commit in commits %}
{%- if not commit.scope %}
{{ self::commit(commit=commit) }}
{%- endif -%}
{%- endfor -%}
{%- endfor %}
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
### New Contributors
{%- endif %}\
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
* @{{ contributor.username }} made their first contribution
{%- if contributor.pr_number %} in \
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
{%- endif %}
{%- endfor -%}
{% if version %}
{% if previous.version %}
**Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}
{% endif %}
{% else -%}
{% raw %}\n{% endraw %}
{% endif %}
{%- macro remote_url() -%}
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\
{% endmacro %}
"""
# remove the leading and trailing whitespace from the template
trim = false
# changelog footer
footer = """
<!-- generated by git-cliff -->
"""
postprocessors = [
{ pattern = '<!-- Please read CONTRIBUTING.md before submitting any pull request. -->', replace = "" },
{ pattern = '>---+\n', replace = '' },
{ pattern = ' +\n', replace = "\n" },
]
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
{ pattern = '(better safe shared layout cache)', replace = "perf(layout): ${1}" },
{ pattern = '(Clarify README.md)', replace = "docs(readme): ${1}" },
{ pattern = '(Update README.md)', replace = "docs(readme): ${1}" },
{ pattern = '(fix typos|Fix typos)', replace = "fix: ${1}" },
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "<!-- 00 -->Features" },
{ message = "^[fF]ix", group = "<!-- 01 -->Bug Fixes" },
{ message = "^refactor", group = "<!-- 02 -->Refactor" },
{ message = "^doc", group = "<!-- 03 -->Documentation" },
{ message = "^perf", group = "<!-- 04 -->Performance" },
{ message = "^style", group = "<!-- 05 -->Styling" },
{ message = "^test", group = "<!-- 06 -->Testing" },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore\\(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" },
{ message = "^ci", group = "<!-- 10 -->Continuous Integration" },
{ message = "^revert", group = "<!-- 11 -->Reverted Commits" },
# handle some old commits styles from pre 0.4
{ message = "^(Buffer|buffer|Frame|frame|Gauge|gauge|Paragraph|paragraph):", group = "<!-- 07 -->Miscellaneous Tasks" },
{ message = "^\\[", group = "<!-- 07 -->Miscellaneous Tasks" },
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers
filter_commits = false
# glob pattern for matching git tags
tag_pattern = "v[0-9]*"
# regex for skipping tags
skip_tags = "v0.1.0-rc.1"
# regex for ignoring tags
ignore_tags = "alpha"
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "newest"

17
clippy.toml Normal file
View File

@@ -0,0 +1,17 @@
avoid-breaking-exported-api = false
# https://rust-lang.github.io/rust-clippy/master/index.html#/multiple_crate_versions
# ratatui -> bitflags v2.3
# termwiz -> wezterm-blob-leases -> mac_address -> nix -> bitflags v1.3.2
# crossterm -> all the windows- deps https://github.com/ratatui/ratatui/pull/1064#issuecomment-2078848980
allowed-duplicate-crates = [
"bitflags",
"windows-targets",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]

14
codecov.yml Normal file
View File

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

30
committed.toml Normal file
View File

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

25
deny.toml Normal file
View File

@@ -0,0 +1,25 @@
# configuration for https://github.com/EmbarkStudios/cargo-deny
[licenses]
version = 2
confidence-threshold = 0.8
allow = [
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"MIT",
"Unicode-DFS-2016",
"WTFPL",
]
[advisories]
version = 2
[bans]
multiple-versions = "allow"
[sources]
unknown-registry = "deny"
unknown-git = "warn"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]

478
examples/README.md Normal file
View File

@@ -0,0 +1,478 @@
# Examples
This folder might use unreleased code. View the examples for the latest release instead.
> [!WARNING]
>
> There may be backwards incompatible changes in these examples, as they are designed to compile
> against the `main` branch.
>
> There are a few workaround for this problem:
>
> - View the examples as they were when the latest version was release by selecting the tag that
> matches that version. E.g. <https://github.com/ratatui/ratatui/tree/v0.26.1/examples>.
> - If you're viewing this file on GitHub, there is a combo box at the top of this page which
> allows you to select any previous tagged version.
> - To view the code locally, checkout the tag. E.g. `git switch --detach v0.26.1`.
> - Use the latest [alpha version of Ratatui] in your app. These are released weekly on Saturdays.
> - Compile your code against the main branch either locally by adding e.g. `path = "../ratatui"` to
> the dependency, or remotely by adding `git = "https://github.com/ratatui/ratatui"`
>
> For a list of unreleased breaking changes, see [BREAKING-CHANGES.md].
>
> We don't keep the CHANGELOG updated with unreleased changes, check the git commit history or run
> `git-cliff -u` against a cloned version of this repository.
## Design choices
The examples contain some opinionated choices in order to make it easier for newer rustaceans to
easily be productive in creating applications:
- Each example has an App struct, with methods that implement a main loop, handle events and drawing
the UI.
- We use color_eyre for handling errors and panics. See [How to use color-eyre with Ratatui] on the
website for more information about this.
- Common code is not extracted into a separate file. This makes each example self-contained and easy
to read as a whole.
Not every example has been updated with all these points in mind yet, however over time they will
be. None of the above choices are strictly necessary for Ratatui apps, but these choices make
examples easier to run, maintain and explain. These choices are designed to help newer users fall
into the pit of success when incorporating example code into their own apps. We may also eventually
move some of these design choices into the core of Ratatui to simplify apps.
[How to use color-eyre with Ratatui]: https://ratatui.rs/how-to/develop-apps/color_eyre/
## Demo2
This is the demo example from the main README and crate page. Source: [demo2](./demo2/).
```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
cargo run --example=demo --no-default-features --features=termion
cargo run --example=demo --no-default-features --features=termwiz
```
![Demo][demo.gif]
## 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
```
![Barchart][barchart.gif]
## Barchart (Grouped)
Demonstrates the [`BarChart`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
widget with groups. Source: [barchart-grouped.rs](./barchart-grouped.rs).
```shell
cargo run --example=barchart-grouped --features=crossterm
```
![Barchart Grouped][barchart-grouped.gif]
## 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
```
![Block][block.gif]
## 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"
```
![Calendar][calendar.gif]
## 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
```
![Canvas][canvas.gif]
## 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
```
![Chart][chart.gif]
## 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
```
![Colors][colors.gif]
## 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/ratatui/assets/381361/485e775a-e0b5-4133-899b-1e8aeb56e774>
## Constraint Explorer
Demonstrates the behaviour of each
[`Constraint`](https://docs.rs/ratatui/latest/ratatui/layout/enum.Constraint.html) option with
respect to each other across different `Flex` modes.
```shell
cargo run --example=constraint-explorer --features=crossterm
```
![Constraint Explorer][constraint-explorer.gif]
## Constraints
Demonstrates how to use
[`Constraint`](https://docs.rs/ratatui/latest/ratatui/layout/enum.Constraint.html) options for
defining layout element sizes.
![Constraints][constraints.gif]
```shell
cargo run --example=constraints --features=crossterm
```
## 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
```
![Custom Widget][custom_widget.gif]
## Gauge
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
```
![Gauge][gauge.gif]
## Flex
Demonstrates the different [`Flex`](https://docs.rs/ratatui/latest/ratatui/layout/enum.Flex.html)
modes for controlling layout space distribution.
```shell
cargo run --example=flex --features=crossterm
```
![Flex][flex.gif]
## Line Gauge
Demonstrates the [`Line
Gauge`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.LineGauge.html) widget. Source:
[line_gauge.rs](./line_gauge.rs).
```shell
cargo run --example=line_gauge --features=crossterm
```
![LineGauge][line_gauge.gif]
## Hyperlink
Demonstrates how to use OSC 8 to create hyperlinks in the terminal.
```shell
cargo run --example=hyperlink --features="crossterm unstable-widget-ref"
```
![Hyperlink][hyperlink.gif]
## Inline
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
```
![Inline][inline.gif]
## 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
```
![Layout][layout.gif]
## 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
```
![List][list.gif]
## 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
```
![Modifiers][modifiers.gif]
## Minimal
Demonstrates how to create a minimal `Hello World!` program.
```shell
cargo run --example=minimal --features=crossterm
```
![Minimal][minimal.gif]
## 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
```
![Panic][panic.gif]
## 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
```
![Paragraph][paragraph.gif]
## 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
```
![Popup][popup.gif]
## 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
```
![Scrollbar][scrollbar.gif]
## 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
```
![Sparkline][sparkline.gif]
## 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
```
![Table][table.gif]
## 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
```
![Tabs][tabs.gif]
## Tracing
Demonstrates how to use the [tracing crate](https://crates.io/crates/tracing) for logging. Creates
a file named `tracing.log` in the current directory.
```shell
cargo run --example=tracing --features=crossterm
```
![Tracing][tracing.gif]
## 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
```
![User Input][user_input.gif]
## How to update these examples
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 `images` git branch to avoid bloating the main
repository.
<!--
Links to images to make them easier to update in bulk. Use the following script to update and upload
the examples to the images branch. (Requires push access to the branch).
```shell
examples/vhs/generate.bash
```
-->
[barchart.gif]: https://github.com/ratatui/ratatui/blob/images/examples/barchart.gif?raw=true
[barchart-grouped.gif]: https://github.com/ratatui/ratatui/blob/images/examples/barchart-grouped.gif?raw=true
[block.gif]: https://github.com/ratatui/ratatui/blob/images/examples/block.gif?raw=true
[calendar.gif]: https://github.com/ratatui/ratatui/blob/images/examples/calendar.gif?raw=true
[canvas.gif]: https://github.com/ratatui/ratatui/blob/images/examples/canvas.gif?raw=true
[chart.gif]: https://github.com/ratatui/ratatui/blob/images/examples/chart.gif?raw=true
[colors.gif]: https://github.com/ratatui/ratatui/blob/images/examples/colors.gif?raw=true
[constraint-explorer.gif]: https://github.com/ratatui/ratatui/blob/images/examples/constraint-explorer.gif?raw=true
[constraints.gif]: https://github.com/ratatui/ratatui/blob/images/examples/constraints.gif?raw=true
[custom_widget.gif]: https://github.com/ratatui/ratatui/blob/images/examples/custom_widget.gif?raw=true
[demo.gif]: https://github.com/ratatui/ratatui/blob/images/examples/demo.gif?raw=true
[demo2.gif]: https://github.com/ratatui/ratatui/blob/images/examples/demo2.gif?raw=true
[flex.gif]: https://github.com/ratatui/ratatui/blob/images/examples/flex.gif?raw=true
[gauge.gif]: https://github.com/ratatui/ratatui/blob/images/examples/gauge.gif?raw=true
[hello_world.gif]: https://github.com/ratatui/ratatui/blob/images/examples/hello_world.gif?raw=true
[hyperlink.gif]: https://github.com/ratatui/ratatui/blob/images/examples/hyperlink.gif?raw=true
[inline.gif]: https://github.com/ratatui/ratatui/blob/images/examples/inline.gif?raw=true
[layout.gif]: https://github.com/ratatui/ratatui/blob/images/examples/layout.gif?raw=true
[list.gif]: https://github.com/ratatui/ratatui/blob/images/examples/list.gif?raw=true
[line_gauge.gif]: https://github.com/ratatui/ratatui/blob/images/examples/line_gauge.gif?raw=true
[minimal.gif]: https://github.com/ratatui/ratatui/blob/images/examples/minimal.gif?raw=true
[modifiers.gif]: https://github.com/ratatui/ratatui/blob/images/examples/modifiers.gif?raw=true
[panic.gif]: https://github.com/ratatui/ratatui/blob/images/examples/panic.gif?raw=true
[paragraph.gif]: https://github.com/ratatui/ratatui/blob/images/examples/paragraph.gif?raw=true
[popup.gif]: https://github.com/ratatui/ratatui/blob/images/examples/popup.gif?raw=true
[ratatui-logo.gif]: https://github.com/ratatui/ratatui/blob/images/examples/ratatui-logo.gif?raw=true
[scrollbar.gif]: https://github.com/ratatui/ratatui/blob/images/examples/scrollbar.gif?raw=true
[sparkline.gif]: https://github.com/ratatui/ratatui/blob/images/examples/sparkline.gif?raw=true
[table.gif]: https://vhs.charm.sh/vhs-6njXBytDf0rwPufUtmSSpI.gif
[tabs.gif]: https://github.com/ratatui/ratatui/blob/images/examples/tabs.gif?raw=true
[tracing.gif]: https://github.com/ratatui/ratatui/blob/images/examples/tracing.gif?raw=true
[user_input.gif]: https://github.com/ratatui/ratatui/blob/images/examples/user_input.gif?raw=true
[alpha version of Ratatui]: https://crates.io/crates/ratatui/versions
[BREAKING-CHANGES.md]: https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md

258
examples/async.rs Normal file
View File

@@ -0,0 +1,258 @@
//! # [Ratatui] Async example
//!
//! This example demonstrates how to use Ratatui with widgets that fetch data asynchronously. It
//! uses the `octocrab` crate to fetch a list of pull requests from the GitHub API. You will need an
//! environment variable named `GITHUB_TOKEN` with a valid GitHub personal access token. The token
//! does not need any special permissions.
//!
//! <https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token>
//! <https://github.com/settings/tokens/new> to create a new token (select classic, and no scopes)
//!
//! This example does not cover message passing between threads, it only demonstrates how to manage
//! shared state between the main thread and a background task, which acts mostly as a one-shot
//! fetcher. For more complex scenarios, you may need to use channels or other synchronization
//! primitives.
//!
//! A simple app might have multiple widgets that fetch data from different sources, and each widget
//! would have its own background task to fetch the data. The main thread would then render the
//! widgets with the latest data.
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use std::{
sync::{Arc, RwLock},
time::Duration,
};
use color_eyre::{eyre::Context, Result, Section};
use futures::StreamExt;
use octocrab::{
params::{pulls::Sort, Direction},
OctocrabBuilder, Page,
};
use ratatui::{
buffer::Buffer,
crossterm::event::{Event, EventStream, KeyCode, KeyEventKind},
layout::{Constraint, Layout, Rect},
style::{Style, Stylize},
text::Line,
widgets::{Block, HighlightSpacing, Row, StatefulWidget, Table, TableState, Widget},
DefaultTerminal, Frame,
};
#[tokio::main]
async fn main() -> Result<()> {
color_eyre::install()?;
init_octocrab()?;
let terminal = ratatui::init();
let app_result = App::default().run(terminal).await;
ratatui::restore();
app_result
}
fn init_octocrab() -> Result<()> {
let token = std::env::var("GITHUB_TOKEN")
.wrap_err("The GITHUB_TOKEN environment variable was not found")
.suggestion(
"Go to https://github.com/settings/tokens/new to create a token, and re-run:
GITHUB_TOKEN=ghp_... cargo run --example async --features crossterm",
)?;
let crab = OctocrabBuilder::new().personal_token(token).build()?;
octocrab::initialise(crab);
Ok(())
}
#[derive(Debug, Default)]
struct App {
should_quit: bool,
pull_requests: PullRequestListWidget,
}
impl App {
const FRAMES_PER_SECOND: f32 = 60.0;
pub async fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
self.pull_requests.run();
let period = Duration::from_secs_f32(1.0 / Self::FRAMES_PER_SECOND);
let mut interval = tokio::time::interval(period);
let mut events = EventStream::new();
while !self.should_quit {
tokio::select! {
_ = interval.tick() => { terminal.draw(|frame| self.draw(frame))?; },
Some(Ok(event)) = events.next() => self.handle_event(&event),
}
}
Ok(())
}
fn draw(&self, frame: &mut Frame) {
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]);
let [title_area, body_area] = vertical.areas(frame.area());
let title = Line::from("Ratatui async example").centered().bold();
frame.render_widget(title, title_area);
frame.render_widget(&self.pull_requests, body_area);
}
fn handle_event(&mut self, event: &Event) {
if let Event::Key(key) = event {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true,
KeyCode::Char('j') | KeyCode::Down => self.pull_requests.scroll_down(),
KeyCode::Char('k') | KeyCode::Up => self.pull_requests.scroll_up(),
_ => {}
}
}
}
}
}
/// A widget that displays a list of pull requests.
///
/// This is an async widget that fetches the list of pull requests from the GitHub API. It contains
/// an inner `Arc<RwLock<PullRequestListState>>` that holds the state of the widget. Cloning the
/// widget will clone the Arc, so you can pass it around to other threads, and this is used to spawn
/// a background task to fetch the pull requests.
#[derive(Debug, Clone, Default)]
struct PullRequestListWidget {
state: Arc<RwLock<PullRequestListState>>,
}
#[derive(Debug, Default)]
struct PullRequestListState {
pull_requests: Vec<PullRequest>,
loading_state: LoadingState,
table_state: TableState,
}
#[derive(Debug, Clone)]
struct PullRequest {
id: String,
title: String,
url: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
enum LoadingState {
#[default]
Idle,
Loading,
Loaded,
Error(String),
}
impl PullRequestListWidget {
/// Start fetching the pull requests in the background.
///
/// This method spawns a background task that fetches the pull requests from the GitHub API.
/// The result of the fetch is then passed to the `on_load` or `on_err` methods.
fn run(&self) {
let this = self.clone(); // clone the widget to pass to the background task
tokio::spawn(this.fetch_pulls());
}
async fn fetch_pulls(self) {
// this runs once, but you could also run this in a loop, using a channel that accepts
// messages to refresh on demand, or with an interval timer to refresh every N seconds
self.set_loading_state(LoadingState::Loading);
match octocrab::instance()
.pulls("ratatui", "ratatui")
.list()
.sort(Sort::Updated)
.direction(Direction::Descending)
.send()
.await
{
Ok(page) => self.on_load(&page),
Err(err) => self.on_err(&err),
}
}
fn on_load(&self, page: &Page<OctoPullRequest>) {
let prs = page.items.iter().map(Into::into);
let mut state = self.state.write().unwrap();
state.loading_state = LoadingState::Loaded;
state.pull_requests.extend(prs);
if !state.pull_requests.is_empty() {
state.table_state.select(Some(0));
}
}
fn on_err(&self, err: &octocrab::Error) {
self.set_loading_state(LoadingState::Error(err.to_string()));
}
fn set_loading_state(&self, state: LoadingState) {
self.state.write().unwrap().loading_state = state;
}
fn scroll_down(&self) {
self.state.write().unwrap().table_state.scroll_down_by(1);
}
fn scroll_up(&self) {
self.state.write().unwrap().table_state.scroll_up_by(1);
}
}
type OctoPullRequest = octocrab::models::pulls::PullRequest;
impl From<&OctoPullRequest> for PullRequest {
fn from(pr: &OctoPullRequest) -> Self {
Self {
id: pr.number.to_string(),
title: pr.title.as_ref().unwrap().to_string(),
url: pr
.html_url
.as_ref()
.map(ToString::to_string)
.unwrap_or_default(),
}
}
}
impl Widget for &PullRequestListWidget {
fn render(self, area: Rect, buf: &mut Buffer) {
let mut state = self.state.write().unwrap();
// a block with a right aligned title with the loading state on the right
let loading_state = Line::from(format!("{:?}", state.loading_state)).right_aligned();
let block = Block::bordered()
.title("Pull Requests")
.title(loading_state)
.title_bottom("j/k to scroll, q to quit");
// a table with the list of pull requests
let rows = state.pull_requests.iter();
let widths = [
Constraint::Length(5),
Constraint::Fill(1),
Constraint::Max(49),
];
let table = Table::new(rows, widths)
.block(block)
.highlight_spacing(HighlightSpacing::Always)
.highlight_symbol(">>")
.highlight_style(Style::new().on_blue());
StatefulWidget::render(table, area, buf, &mut state.table_state);
}
}
impl From<&PullRequest> for Row<'_> {
fn from(pr: &PullRequest) -> Self {
let pr = pr.clone();
Row::new(vec![pr.id, pr.title, pr.url])
}
}

View File

@@ -0,0 +1,211 @@
//! # [Ratatui] `BarChart` example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use std::iter::zip;
use color_eyre::Result;
use ratatui::{
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Direction, Layout},
style::{Color, Style, Stylize},
text::Line,
widgets::{Bar, BarChart, BarGroup, Block},
DefaultTerminal, Frame,
};
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::new().run(terminal);
ratatui::restore();
app_result
}
const COMPANY_COUNT: usize = 3;
const PERIOD_COUNT: usize = 4;
struct App {
should_exit: bool,
companies: [Company; COMPANY_COUNT],
revenues: [Revenues; PERIOD_COUNT],
}
struct Revenues {
period: &'static str,
revenues: [u32; COMPANY_COUNT],
}
struct Company {
short_name: &'static str,
name: &'static str,
color: Color,
}
impl App {
const fn new() -> Self {
Self {
should_exit: false,
companies: fake_companies(),
revenues: fake_revenues(),
}
}
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
while !self.should_exit {
terminal.draw(|frame| self.draw(frame))?;
self.handle_events()?;
}
Ok(())
}
fn handle_events(&mut self) -> Result<()> {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
self.should_exit = true;
}
}
Ok(())
}
fn draw(&self, frame: &mut Frame) {
use Constraint::{Fill, Length, Min};
let vertical = Layout::vertical([Length(1), Fill(1), Min(20)]).spacing(1);
let [title, top, bottom] = vertical.areas(frame.area());
frame.render_widget("Grouped Barchart".bold().into_centered_line(), title);
frame.render_widget(self.vertical_revenue_barchart(), top);
frame.render_widget(self.horizontal_revenue_barchart(), bottom);
}
/// Create a vertical revenue bar chart with the data from the `revenues` field.
fn vertical_revenue_barchart(&self) -> BarChart<'_> {
let mut barchart = BarChart::default()
.block(Block::new().title(Line::from("Company revenues (Vertical)").centered()))
.bar_gap(0)
.bar_width(6)
.group_gap(2);
for group in self
.revenues
.iter()
.map(|revenue| revenue.to_vertical_bar_group(&self.companies))
{
barchart = barchart.data(group);
}
barchart
}
/// Create a horizontal revenue bar chart with the data from the `revenues` field.
fn horizontal_revenue_barchart(&self) -> BarChart<'_> {
let title = Line::from("Company Revenues (Horizontal)").centered();
let mut barchart = BarChart::default()
.block(Block::new().title(title))
.bar_width(1)
.group_gap(2)
.bar_gap(0)
.direction(Direction::Horizontal);
for group in self
.revenues
.iter()
.map(|revenue| revenue.to_horizontal_bar_group(&self.companies))
{
barchart = barchart.data(group);
}
barchart
}
}
/// Generate fake company data
const fn fake_companies() -> [Company; COMPANY_COUNT] {
[
Company::new("BAKE", "Bake my day", Color::LightRed),
Company::new("BITE", "Bits and Bites", Color::Blue),
Company::new("TART", "Tart of the Table", Color::White),
]
}
/// Some fake revenue data
const fn fake_revenues() -> [Revenues; PERIOD_COUNT] {
[
Revenues::new("Jan", [8500, 6500, 7000]),
Revenues::new("Feb", [9000, 7500, 8500]),
Revenues::new("Mar", [9500, 4500, 8200]),
Revenues::new("Apr", [6300, 4000, 5000]),
]
}
impl Revenues {
/// Create a new instance of `Revenues`
const fn new(period: &'static str, revenues: [u32; COMPANY_COUNT]) -> Self {
Self { period, revenues }
}
/// Create a `BarGroup` with vertical bars for each company
fn to_vertical_bar_group<'a>(&self, companies: &'a [Company]) -> BarGroup<'a> {
let bars: Vec<Bar> = zip(companies, self.revenues)
.map(|(company, revenue)| company.vertical_revenue_bar(revenue))
.collect();
BarGroup::default()
.label(Line::from(self.period).centered())
.bars(&bars)
}
/// Create a `BarGroup` with horizontal bars for each company
fn to_horizontal_bar_group<'a>(&'a self, companies: &'a [Company]) -> BarGroup<'a> {
let bars: Vec<Bar> = zip(companies, self.revenues)
.map(|(company, revenue)| company.horizontal_revenue_bar(revenue))
.collect();
BarGroup::default()
.label(Line::from(self.period).centered())
.bars(&bars)
}
}
impl Company {
/// Create a new instance of `Company`
const fn new(short_name: &'static str, name: &'static str, color: Color) -> Self {
Self {
short_name,
name,
color,
}
}
/// Create a vertical revenue bar for the company
///
/// The label is the short name of the company, and will be displayed under the bar
fn vertical_revenue_bar(&self, revenue: u32) -> Bar {
let text_value = format!("{:.1}M", f64::from(revenue) / 1000.);
Bar::default()
.label(self.short_name.into())
.value(u64::from(revenue))
.text_value(text_value)
.style(self.color)
.value_style(Style::new().fg(Color::Black).bg(self.color))
}
/// Create a horizontal revenue bar for the company
///
/// The label is the long name of the company combined with the revenue and will be displayed
/// on the bar
fn horizontal_revenue_bar(&self, revenue: u32) -> Bar {
let text_value = format!("{} ({:.1} M)", self.name, f64::from(revenue) / 1000.);
Bar::default()
.value(u64::from(revenue))
.text_value(text_value)
.style(self.color)
.value_style(Style::new().fg(Color::Black).bg(self.color))
}
}

View File

@@ -1,125 +1,136 @@
#[allow(dead_code)]
mod util;
//! # [Ratatui] `BarChart` example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use crate::util::event::{Event, Events};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
use color_eyre::Result;
use rand::{thread_rng, Rng};
use ratatui::{
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{BarChart, Block, Borders},
Terminal,
style::{Color, Style, Stylize},
text::Line,
widgets::{Bar, BarChart, BarGroup, Block},
DefaultTerminal, Frame,
};
struct App<'a> {
data: Vec<(&'a str, u64)>,
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::new().run(terminal);
ratatui::restore();
app_result
}
impl<'a> App<'a> {
fn new() -> App<'a> {
App {
data: vec![
("B1", 9),
("B2", 12),
("B3", 5),
("B4", 8),
("B5", 2),
("B6", 4),
("B7", 5),
("B8", 9),
("B9", 14),
("B10", 15),
("B11", 1),
("B12", 0),
("B13", 4),
("B14", 6),
("B15", 4),
("B16", 6),
("B17", 4),
("B18", 7),
("B19", 13),
("B20", 8),
("B21", 11),
("B22", 9),
("B23", 3),
("B24", 5),
],
struct App {
should_exit: bool,
temperatures: Vec<u8>,
}
impl App {
fn new() -> Self {
let mut rng = thread_rng();
let temperatures = (0..24).map(|_| rng.gen_range(50..90)).collect();
Self {
should_exit: false,
temperatures,
}
}
fn update(&mut self) {
let value = self.data.pop().unwrap();
self.data.insert(0, value);
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
while !self.should_exit {
terminal.draw(|frame| self.draw(frame))?;
self.handle_events()?;
}
Ok(())
}
}
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
// Setup event handlers
let events = Events::new();
// App
let mut app = App::new();
loop {
terminal.draw(|mut f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
let barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
.data(&app.data)
.bar_width(9)
.style(Style::default().fg(Color::Yellow))
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
f.render_widget(barchart, chunks[0]);
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]);
let barchart = BarChart::default()
.block(Block::default().title("Data2").borders(Borders::ALL))
.data(&app.data)
.bar_width(5)
.bar_gap(3)
.style(Style::default().fg(Color::Green))
.value_style(Style::default().bg(Color::Green).modifier(Modifier::BOLD));
f.render_widget(barchart, chunks[0]);
let barchart = BarChart::default()
.block(Block::default().title("Data3").borders(Borders::ALL))
.data(&app.data)
.style(Style::default().fg(Color::Red))
.bar_width(7)
.bar_gap(0)
.value_style(Style::default().bg(Color::Red))
.label_style(Style::default().fg(Color::Cyan).modifier(Modifier::ITALIC));
f.render_widget(barchart, chunks[1]);
})?;
match events.next()? {
Event::Input(input) => {
if input == Key::Char('q') {
break;
}
}
Event::Tick => {
app.update();
fn handle_events(&mut self) -> Result<()> {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
self.should_exit = true;
}
}
Ok(())
}
Ok(())
fn draw(&self, frame: &mut Frame) {
let [title, vertical, horizontal] = Layout::vertical([
Constraint::Length(1),
Constraint::Fill(1),
Constraint::Fill(1),
])
.spacing(1)
.areas(frame.area());
frame.render_widget("Barchart".bold().into_centered_line(), title);
frame.render_widget(vertical_barchart(&self.temperatures), vertical);
frame.render_widget(horizontal_barchart(&self.temperatures), horizontal);
}
}
/// Create a vertical bar chart from the temperatures data.
fn vertical_barchart(temperatures: &[u8]) -> BarChart {
let bars: Vec<Bar> = temperatures
.iter()
.enumerate()
.map(|(hour, value)| vertical_bar(hour, value))
.collect();
let title = Line::from("Weather (Vertical)").centered();
BarChart::default()
.data(BarGroup::default().bars(&bars))
.block(Block::new().title(title))
.bar_width(5)
}
fn vertical_bar(hour: usize, temperature: &u8) -> Bar {
Bar::default()
.value(u64::from(*temperature))
.label(Line::from(format!("{hour:>02}:00")))
.text_value(format!("{temperature:>3}°"))
.style(temperature_style(*temperature))
.value_style(temperature_style(*temperature).reversed())
}
/// Create a horizontal bar chart from the temperatures data.
fn horizontal_barchart(temperatures: &[u8]) -> BarChart {
let bars: Vec<Bar> = temperatures
.iter()
.enumerate()
.map(|(hour, value)| horizontal_bar(hour, value))
.collect();
let title = Line::from("Weather (Horizontal)").centered();
BarChart::default()
.block(Block::new().title(title))
.data(BarGroup::default().bars(&bars))
.bar_width(1)
.bar_gap(0)
.direction(Direction::Horizontal)
}
fn horizontal_bar(hour: usize, temperature: &u8) -> Bar {
let style = temperature_style(*temperature);
Bar::default()
.value(u64::from(*temperature))
.label(Line::from(format!("{hour:>02}:00")))
.text_value(format!("{temperature:>3}°"))
.style(style)
.value_style(style.reversed())
}
/// create a yellow to red value based on the value (50-90)
fn temperature_style(value: u8) -> Style {
let green = (255.0 * (1.0 - f64::from(value - 50) / 40.0)) as u8;
let color = Color::Rgb(255, green, 0);
Style::new().fg(color)
}

View File

@@ -1,88 +1,186 @@
#[allow(dead_code)]
mod util;
//! # [Ratatui] Block example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use crate::util::event::{Event, Events};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{Block, BorderType, Borders},
Terminal,
use color_eyre::Result;
use ratatui::{
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Alignment, Constraint, Layout, Rect},
style::{Style, Stylize},
text::Line,
widgets::{Block, BorderType, Borders, Padding, Paragraph, Wrap},
DefaultTerminal, Frame,
};
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
// Setup event handlers
let events = Events::new();
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let result = run(terminal);
ratatui::restore();
result
}
fn run(mut terminal: DefaultTerminal) -> Result<()> {
loop {
terminal.draw(|mut f| {
// Wrapping block for a group
// Just draw the block and the group on the same area and build the group
// with at least a margin of 1
let size = f.size();
let block = Block::default()
.borders(Borders::ALL)
.title("Main block with round corners")
.border_type(BorderType::Rounded);
f.render_widget(block, size);
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(4)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
{
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[0]);
let block = Block::default()
.title("With background")
.title_style(Style::default().fg(Color::Yellow))
.style(Style::default().bg(Color::Green));
f.render_widget(block, chunks[0]);
let title_style = Style::default()
.fg(Color::White)
.bg(Color::Red)
.modifier(Modifier::BOLD);
let block = Block::default()
.title("Styled title")
.title_style(title_style);
f.render_widget(block, chunks[1]);
terminal.draw(draw)?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
break Ok(());
}
{
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]);
let block = Block::default().title("With borders").borders(Borders::ALL);
f.render_widget(block, chunks[0]);
let block = Block::default()
.title("With styled borders and doubled borders")
.border_style(Style::default().fg(Color::Cyan))
.borders(Borders::LEFT | Borders::RIGHT)
.border_type(BorderType::Double);
f.render_widget(block, chunks[1]);
}
})?;
match events.next()? {
Event::Input(key) => {
if key == Key::Char('q') {
break;
}
}
_ => {}
}
}
Ok(())
}
fn draw(frame: &mut Frame) {
let (title_area, layout) = calculate_layout(frame.area());
render_title(frame, title_area);
let paragraph = placeholder_paragraph();
render_borders(&paragraph, Borders::ALL, frame, layout[0][0]);
render_borders(&paragraph, Borders::NONE, frame, layout[0][1]);
render_borders(&paragraph, Borders::LEFT, frame, layout[1][0]);
render_borders(&paragraph, Borders::RIGHT, frame, layout[1][1]);
render_borders(&paragraph, Borders::TOP, frame, layout[2][0]);
render_borders(&paragraph, Borders::BOTTOM, frame, layout[2][1]);
render_border_type(&paragraph, BorderType::Plain, frame, layout[3][0]);
render_border_type(&paragraph, BorderType::Rounded, frame, layout[3][1]);
render_border_type(&paragraph, BorderType::Double, frame, layout[4][0]);
render_border_type(&paragraph, BorderType::Thick, frame, layout[4][1]);
render_styled_block(&paragraph, frame, layout[5][0]);
render_styled_borders(&paragraph, frame, layout[5][1]);
render_styled_title(&paragraph, frame, layout[6][0]);
render_styled_title_content(&paragraph, frame, layout[6][1]);
render_multiple_titles(&paragraph, frame, layout[7][0]);
render_multiple_title_positions(&paragraph, frame, layout[7][1]);
render_padding(&paragraph, frame, layout[8][0]);
render_nested_blocks(&paragraph, frame, layout[8][1]);
}
/// Calculate the layout of the UI elements.
///
/// Returns a tuple of the title area and the main areas.
fn calculate_layout(area: Rect) -> (Rect, Vec<Vec<Rect>>) {
let main_layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
let block_layout = Layout::vertical([Constraint::Max(4); 9]);
let [title_area, main_area] = main_layout.areas(area);
let main_areas = block_layout
.split(main_area)
.iter()
.map(|&area| {
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(area)
.to_vec()
})
.collect();
(title_area, main_areas)
}
fn render_title(frame: &mut Frame, area: Rect) {
frame.render_widget(
Paragraph::new("Block example. Press q to quit")
.dark_gray()
.alignment(Alignment::Center),
area,
);
}
fn placeholder_paragraph() -> Paragraph<'static> {
let text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
Paragraph::new(text.dark_gray()).wrap(Wrap { trim: true })
}
fn render_borders(paragraph: &Paragraph, border: Borders, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(border)
.title(format!("Borders::{border:#?}"));
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_border_type(
paragraph: &Paragraph,
border_type: BorderType,
frame: &mut Frame,
area: Rect,
) {
let block = Block::bordered()
.border_type(border_type)
.title(format!("BorderType::{border_type:#?}"));
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_styled_borders(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::bordered()
.border_style(Style::new().blue().on_white().bold().italic())
.title("Styled borders");
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_styled_block(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::bordered()
.style(Style::new().blue().on_white().bold().italic())
.title("Styled block");
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_styled_title(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::bordered()
.title("Styled title")
.title_style(Style::new().blue().on_white().bold().italic());
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_styled_title_content(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let title = Line::from(vec![
"Styled ".blue().on_white().bold().italic(),
"title content".red().on_white().bold().italic(),
]);
let block = Block::bordered().title(title);
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_multiple_titles(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::bordered()
.title("Multiple".blue().on_white().bold().italic())
.title("Titles".red().on_white().bold().italic());
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_multiple_title_positions(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::bordered()
.title(Line::from("top left").left_aligned())
.title(Line::from("top center").centered())
.title(Line::from("top right").right_aligned())
.title_bottom(Line::from("bottom left").left_aligned())
.title_bottom(Line::from("bottom center").centered())
.title_bottom(Line::from("bottom right").right_aligned());
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_padding(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::bordered()
.padding(Padding::new(5, 10, 1, 2))
.title("Padding");
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_nested_blocks(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let outer_block = Block::bordered().title("Outer block");
let inner_block = Block::bordered().title("Inner block");
let inner = outer_block.inner(area);
frame.render_widget(outer_block, area);
frame.render_widget(paragraph.clone().block(inner_block), inner);
}

249
examples/calendar.rs Normal file
View File

@@ -0,0 +1,249 @@
//! # [Ratatui] Calendar example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use color_eyre::Result;
use ratatui::{
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Layout, Margin},
style::{Color, Modifier, Style},
widgets::calendar::{CalendarEventStore, DateStyler, Monthly},
DefaultTerminal, Frame,
};
use time::{Date, Month, OffsetDateTime};
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let result = run(terminal);
ratatui::restore();
result
}
fn run(mut terminal: DefaultTerminal) -> Result<()> {
loop {
terminal.draw(draw)?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
break Ok(());
}
}
}
}
fn draw(frame: &mut Frame) {
let area = frame.area().inner(Margin {
vertical: 1,
horizontal: 1,
});
let mut start = OffsetDateTime::now_local()
.unwrap()
.date()
.replace_month(Month::January)
.unwrap()
.replace_day(1)
.unwrap();
let list = make_dates(start.year());
let rows = Layout::vertical([Constraint::Ratio(1, 3); 3]).split(area);
let cols = rows.iter().flat_map(|row| {
Layout::horizontal([Constraint::Ratio(1, 4); 4])
.split(*row)
.to_vec()
});
for col in cols {
let cal = cals::get_cal(start.month(), start.year(), &list);
frame.render_widget(cal, col);
start = start.replace_month(start.month().next()).unwrap();
}
}
fn make_dates(current_year: i32) -> CalendarEventStore {
let mut list = CalendarEventStore::today(
Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Blue),
);
// Holidays
let holiday_style = Style::default()
.fg(Color::Red)
.add_modifier(Modifier::UNDERLINED);
// new year's
list.add(
Date::from_calendar_date(current_year, Month::January, 1).unwrap(),
holiday_style,
);
// next new_year's for December "show surrounding"
list.add(
Date::from_calendar_date(current_year + 1, Month::January, 1).unwrap(),
holiday_style,
);
// groundhog day
list.add(
Date::from_calendar_date(current_year, Month::February, 2).unwrap(),
holiday_style,
);
// april fool's
list.add(
Date::from_calendar_date(current_year, Month::April, 1).unwrap(),
holiday_style,
);
// earth day
list.add(
Date::from_calendar_date(current_year, Month::April, 22).unwrap(),
holiday_style,
);
// star wars day
list.add(
Date::from_calendar_date(current_year, Month::May, 4).unwrap(),
holiday_style,
);
// festivus
list.add(
Date::from_calendar_date(current_year, Month::December, 23).unwrap(),
holiday_style,
);
// new year's eve
list.add(
Date::from_calendar_date(current_year, Month::December, 31).unwrap(),
holiday_style,
);
// seasons
let season_style = Style::default()
.fg(Color::White)
.bg(Color::Yellow)
.add_modifier(Modifier::UNDERLINED);
// spring equinox
list.add(
Date::from_calendar_date(current_year, Month::March, 22).unwrap(),
season_style,
);
// summer solstice
list.add(
Date::from_calendar_date(current_year, Month::June, 21).unwrap(),
season_style,
);
// fall equinox
list.add(
Date::from_calendar_date(current_year, Month::September, 22).unwrap(),
season_style,
);
list.add(
Date::from_calendar_date(current_year, Month::December, 21).unwrap(),
season_style,
);
list
}
mod cals {
#[allow(clippy::wildcard_imports)]
use super::*;
pub fn get_cal<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
match m {
Month::May => example1(m, y, es),
Month::June => example2(m, y, es),
Month::July | Month::December => example3(m, y, es),
Month::February => example4(m, y, es),
Month::November => example5(m, y, es),
_ => default(m, y, es),
}
}
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));
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
.show_month_header(Style::default())
.default_style(default_style)
}
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));
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
.show_surrounding(default_style)
.default_style(default_style)
.show_month_header(Style::default())
}
fn example2<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
let header_style = Style::default()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::DIM)
.fg(Color::LightYellow);
let default_style = Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Rgb(50, 50, 50));
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
.show_weekdays_header(header_style)
.default_style(default_style)
.show_month_header(Style::default())
}
fn example3<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
let header_style = Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Green);
let default_style = Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Rgb(50, 50, 50));
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
.show_surrounding(Style::default().add_modifier(Modifier::DIM))
.show_weekdays_header(header_style)
.default_style(default_style)
.show_month_header(Style::default())
}
fn example4<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
let header_style = Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Green);
let default_style = Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Rgb(50, 50, 50));
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
.show_weekdays_header(header_style)
.default_style(default_style)
}
fn example5<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
let header_style = Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Green);
let default_style = Style::default()
.add_modifier(Modifier::BOLD)
.bg(Color::Rgb(50, 50, 50));
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
.show_month_header(header_style)
.default_style(default_style)
}
}

View File

@@ -1,149 +1,200 @@
#[allow(dead_code)]
mod util;
//! # [Ratatui] Canvas example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use crate::util::event::{Config, Event, Events};
use std::{error::Error, io, time::Duration};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
layout::{Constraint, Direction, Layout, Rect},
style::Color,
use std::time::{Duration, Instant};
use color_eyre::Result;
use ratatui::{
crossterm::event::{self, Event, KeyCode},
layout::{Constraint, Layout, Rect},
style::{Color, Stylize},
symbols::Marker,
widgets::{
canvas::{Canvas, Map, MapResolution, Rectangle},
Block, Borders,
canvas::{Canvas, Circle, Map, MapResolution, Rectangle},
Block, Widget,
},
Terminal,
DefaultTerminal, Frame,
};
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::new().run(terminal);
ratatui::restore();
app_result
}
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,
}
impl App {
fn new() -> App {
App {
fn new() -> Self {
Self {
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 update(&mut self) {
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>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
// Setup event handlers
let config = Config {
tick_rate: Duration::from_millis(250),
..Default::default()
};
let events = Events::with_config(config);
// App
let mut app = App::new();
loop {
terminal.draw(|mut f| {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
let canvas = Canvas::default()
.block(Block::default().borders(Borders::ALL).title("World"))
.paint(|ctx| {
ctx.draw(&Map {
color: Color::White,
resolution: MapResolution::High,
});
ctx.print(app.x, -app.y, "You are here", Color::Yellow);
})
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0]);
f.render_widget(canvas, chunks[0]);
let canvas = Canvas::default()
.block(Block::default().borders(Borders::ALL).title("Pong"))
.paint(|ctx| {
ctx.draw(&app.ball);
})
.x_bounds([10.0, 110.0])
.y_bounds([10.0, 110.0]);
f.render_widget(canvas, chunks[1]);
})?;
match events.next()? {
Event::Input(input) => match input {
Key::Char('q') => {
break;
}
Key::Down => {
app.y += 1.0;
}
Key::Up => {
app.y -= 1.0;
}
Key::Right => {
app.x += 1.0;
}
Key::Left => {
app.x -= 1.0;
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
let tick_rate = Duration::from_millis(16);
let mut last_tick = Instant::now();
loop {
terminal.draw(|frame| self.draw(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 Ok(()),
KeyCode::Down | KeyCode::Char('j') => self.y += 1.0,
KeyCode::Up | KeyCode::Char('k') => self.y -= 1.0,
KeyCode::Right | KeyCode::Char('l') => self.x += 1.0,
KeyCode::Left | KeyCode::Char('h') => self.x -= 1.0,
_ => {}
}
}
}
_ => {}
},
Event::Tick => {
app.update();
if last_tick.elapsed() >= tick_rate {
self.on_tick();
last_tick = Instant::now();
}
}
}
Ok(())
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 < f64::from(playground.left())
|| ball.x + ball.radius > f64::from(playground.right())
{
self.vx = -self.vx;
}
if ball.y - ball.radius < f64::from(playground.top())
|| ball.y + ball.radius > f64::from(playground.bottom())
{
self.vy = -self.vy;
}
self.ball.x += self.vx;
self.ball.y += self.vy;
}
fn draw(&self, frame: &mut Frame) {
let horizontal =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [map, right] = horizontal.areas(frame.area());
let [pong, boxes] = vertical.areas(right);
frame.render_widget(self.map_canvas(), map);
frame.render_widget(self.pong_canvas(), pong);
frame.render_widget(self.boxes_canvas(boxes), boxes);
}
fn map_canvas(&self) -> impl Widget + '_ {
Canvas::default()
.block(Block::bordered().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::bordered().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 = 0.0;
let right = f64::from(area.width);
let bottom = 0.0;
let top = f64::from(area.height).mul_add(2.0, -4.0);
Canvas::default()
.block(Block::bordered().title("Rects"))
.marker(self.marker)
.x_bounds([left, right])
.y_bounds([bottom, top])
.paint(|ctx| {
for i in 0..=11 {
ctx.draw(&Rectangle {
x: f64::from(i * i + 3 * i) / 2.0 + 2.0,
y: 2.0,
width: f64::from(i),
height: f64::from(i),
color: Color::Red,
});
ctx.draw(&Rectangle {
x: f64::from(i * i + 3 * i) / 2.0 + 2.0,
y: 21.0,
width: f64::from(i),
height: f64::from(i),
color: Color::Blue,
});
}
for i in 0..100 {
if i % 10 != 0 {
ctx.print(f64::from(i) + 1.0, 0.0, format!("{i}", i = i % 10));
}
if i % 2 == 0 && i % 10 != 0 {
ctx.print(0.0, f64::from(i), format!("{i}", i = i % 10));
}
}
})
}
}

View File

@@ -1,31 +1,38 @@
#[allow(dead_code)]
mod util;
//! # [Ratatui] Chart example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use crate::util::{
event::{Event, Events},
SinSignal,
};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
symbols,
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType},
Terminal,
use std::time::{Duration, Instant};
use color_eyre::Result;
use ratatui::{
crossterm::event::{self, Event, KeyCode},
layout::{Constraint, Layout, Rect},
style::{Color, Modifier, Style, Stylize},
symbols::{self, Marker},
text::{Line, Span},
widgets::{Axis, Block, Chart, Dataset, GraphType, LegendPosition},
DefaultTerminal, Frame,
};
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),
];
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::new().run(terminal);
ratatui::restore();
app_result
}
struct App {
signal1: SinSignal,
@@ -35,13 +42,41 @@ struct App {
window: [f64; 2],
}
#[derive(Clone)]
struct SinSignal {
x: f64,
interval: f64,
period: f64,
scale: f64,
}
impl SinSignal {
const fn new(interval: f64, period: f64, scale: f64) -> Self {
Self {
x: 0.0,
interval,
period,
scale,
}
}
}
impl Iterator for SinSignal {
type Item = (f64, f64);
fn next(&mut self) -> Option<Self::Item> {
let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
self.x += self.interval;
Some(point)
}
}
impl App {
fn new() -> App {
fn new() -> Self {
let mut signal1 = SinSignal::new(0.2, 3.0, 18.0);
let mut signal2 = SinSignal::new(0.1, 2.0, 10.0);
let data1 = signal1.by_ref().take(200).collect::<Vec<(f64, f64)>>();
let data2 = signal2.by_ref().take(200).collect::<Vec<(f64, f64)>>();
App {
Self {
signal1,
data1,
signal2,
@@ -50,167 +85,275 @@ impl App {
}
}
fn update(&mut self) {
for _ in 0..5 {
self.data1.remove(0);
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
let tick_rate = Duration::from_millis(250);
let mut last_tick = Instant::now();
loop {
terminal.draw(|frame| self.draw(frame))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
self.on_tick();
last_tick = Instant::now();
}
}
}
fn on_tick(&mut self) {
self.data1.drain(0..5);
self.data1.extend(self.signal1.by_ref().take(5));
for _ in 0..10 {
self.data2.remove(0);
}
self.data2.drain(0..10);
self.data2.extend(self.signal2.by_ref().take(10));
self.window[0] += 1.0;
self.window[1] += 1.0;
}
}
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
fn draw(&self, frame: &mut Frame) {
let [top, bottom] = Layout::vertical([Constraint::Fill(1); 2]).areas(frame.area());
let [animated_chart, bar_chart] =
Layout::horizontal([Constraint::Fill(1), Constraint::Length(29)]).areas(top);
let [line_chart, scatter] = Layout::horizontal([Constraint::Fill(1); 2]).areas(bottom);
let events = Events::new();
// App
let mut app = App::new();
loop {
terminal.draw(|mut f| {
let size = f.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
]
.as_ref(),
)
.split(size);
let x_labels = [
format!("{}", app.window[0]),
format!("{}", (app.window[0] + app.window[1]) / 2.0),
format!("{}", app.window[1]),
];
let datasets = [
Dataset::default()
.name("data2")
.marker(symbols::Marker::Dot)
.style(Style::default().fg(Color::Cyan))
.data(&app.data1),
Dataset::default()
.name("data3")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.data(&app.data2),
];
let chart = Chart::default()
.block(
Block::default()
.title("Chart 1")
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD))
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds(app.window)
.labels(&x_labels),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([-20.0, 20.0])
.labels(&["-20", "0", "20"]),
)
.datasets(&datasets);
f.render_widget(chart, chunks[0]);
let datasets = [Dataset::default()
.name("data")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.graph_type(GraphType::Line)
.data(&DATA)];
let chart = Chart::default()
.block(
Block::default()
.title("Chart 2")
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD))
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([0.0, 5.0])
.labels(&["0", "2.5", "5.0"]),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([0.0, 5.0])
.labels(&["0", "2.5", "5.0"]),
)
.datasets(&datasets);
f.render_widget(chart, chunks[1]);
let datasets = [Dataset::default()
.name("data")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.graph_type(GraphType::Line)
.data(&DATA2)];
let chart = Chart::default()
.block(
Block::default()
.title("Chart 3")
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD))
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([0.0, 50.0])
.labels(&["0", "25", "50"]),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([0.0, 5.0])
.labels(&["0", "2.5", "5"]),
)
.datasets(&datasets);
f.render_widget(chart, chunks[2]);
})?;
match events.next()? {
Event::Input(input) => {
if input == Key::Char('q') {
break;
}
}
Event::Tick => {
app.update();
}
}
self.render_animated_chart(frame, animated_chart);
render_barchart(frame, bar_chart);
render_line_chart(frame, line_chart);
render_scatter(frame, scatter);
}
Ok(())
fn render_animated_chart(&self, frame: &mut Frame, area: Rect) {
let x_labels = vec![
Span::styled(
format!("{}", self.window[0]),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(format!("{}", (self.window[0] + self.window[1]) / 2.0)),
Span::styled(
format!("{}", self.window[1]),
Style::default().add_modifier(Modifier::BOLD),
),
];
let datasets = vec![
Dataset::default()
.name("data2")
.marker(symbols::Marker::Dot)
.style(Style::default().fg(Color::Cyan))
.data(&self.data1),
Dataset::default()
.name("data3")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.data(&self.data2),
];
let chart = Chart::new(datasets)
.block(Block::bordered())
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.labels(x_labels)
.bounds(self.window),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.labels(["-20".bold(), "0".into(), "20".bold()])
.bounds([-20.0, 20.0]),
);
frame.render_widget(chart, area);
}
}
fn render_barchart(frame: &mut Frame, bar_chart: Rect) {
let dataset = Dataset::default()
.marker(symbols::Marker::HalfBlock)
.style(Style::new().fg(Color::Blue))
.graph_type(GraphType::Bar)
// a bell curve
.data(&[
(0., 0.4),
(10., 2.9),
(20., 13.5),
(30., 41.1),
(40., 80.1),
(50., 100.0),
(60., 80.1),
(70., 41.1),
(80., 13.5),
(90., 2.9),
(100., 0.4),
]);
let chart = Chart::new(vec![dataset])
.block(Block::bordered().title_top(Line::from("Bar chart").cyan().bold().centered()))
.x_axis(
Axis::default()
.style(Style::default().gray())
.bounds([0.0, 100.0])
.labels(["0".bold(), "50".into(), "100.0".bold()]),
)
.y_axis(
Axis::default()
.style(Style::default().gray())
.bounds([0.0, 100.0])
.labels(["0".bold(), "50".into(), "100.0".bold()]),
)
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
frame.render_widget(chart, bar_chart);
}
fn render_line_chart(frame: &mut Frame, area: Rect) {
let datasets = vec![Dataset::default()
.name("Line from only 2 points".italic())
.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::bordered().title(Line::from("Line chart").cyan().bold().centered()))
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().gray())
.bounds([0.0, 5.0])
.labels(["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(["0".bold(), "2.5".into(), "5.0".bold()]),
)
.legend_position(Some(LegendPosition::TopLeft))
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
frame.render_widget(chart, area);
}
fn render_scatter(frame: &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".underlined())
.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::bordered().title(Line::from("Scatter chart").cyan().bold().centered()))
.x_axis(
Axis::default()
.title("Year")
.bounds([1960., 2020.])
.style(Style::default().fg(Color::Gray))
.labels(["1960", "1990", "2020"]),
)
.y_axis(
Axis::default()
.title("Cost")
.bounds([0., 75000.])
.style(Style::default().fg(Color::Gray))
.labels(["0", "37 500", "75 000"]),
)
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
frame.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., 177_900.),
(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., 118_500.),
(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.),
];

249
examples/colors.rs Normal file
View File

@@ -0,0 +1,249 @@
//! # [Ratatui] Colors example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
// This example shows all the colors supported by ratatui. It will render a grid of foreground
// and background colors with their names and indexes.
use color_eyre::Result;
use itertools::Itertools;
use ratatui::{
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Alignment, Constraint, Layout, Rect},
style::{Color, Style, Stylize},
text::Line,
widgets::{Block, Borders, Paragraph},
DefaultTerminal, Frame,
};
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = run(terminal);
ratatui::restore();
app_result
}
fn run(mut terminal: DefaultTerminal) -> Result<()> {
loop {
terminal.draw(draw)?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(());
}
}
}
}
fn draw(frame: &mut Frame) {
let layout = Layout::vertical([
Constraint::Length(30),
Constraint::Length(17),
Constraint::Length(2),
])
.split(frame.area());
render_named_colors(frame, layout[0]);
render_indexed_colors(frame, layout[1]);
render_indexed_grayscale(frame, layout[2]);
}
const NAMED_COLORS: [Color; 16] = [
Color::Black,
Color::Red,
Color::Green,
Color::Yellow,
Color::Blue,
Color::Magenta,
Color::Cyan,
Color::Gray,
Color::DarkGray,
Color::LightRed,
Color::LightGreen,
Color::LightYellow,
Color::LightBlue,
Color::LightMagenta,
Color::LightCyan,
Color::White,
];
fn render_named_colors(frame: &mut Frame, area: Rect) {
let layout = Layout::vertical([Constraint::Length(3); 10]).split(area);
render_fg_named_colors(frame, Color::Reset, layout[0]);
render_fg_named_colors(frame, Color::Black, layout[1]);
render_fg_named_colors(frame, Color::DarkGray, layout[2]);
render_fg_named_colors(frame, Color::Gray, layout[3]);
render_fg_named_colors(frame, Color::White, layout[4]);
render_bg_named_colors(frame, Color::Reset, layout[5]);
render_bg_named_colors(frame, Color::Black, layout[6]);
render_bg_named_colors(frame, Color::DarkGray, layout[7]);
render_bg_named_colors(frame, Color::Gray, layout[8]);
render_bg_named_colors(frame, Color::White, layout[9]);
}
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 vertical = Layout::vertical([Constraint::Length(1); 2]).split(inner);
let areas = vertical.iter().flat_map(|area| {
Layout::horizontal([Constraint::Ratio(1, 8); 8])
.split(*area)
.to_vec()
});
for (fg, area) in NAMED_COLORS.into_iter().zip(areas) {
let color_name = fg.to_string();
let paragraph = Paragraph::new(color_name).fg(fg).bg(bg);
frame.render_widget(paragraph, area);
}
}
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 vertical = Layout::vertical([Constraint::Length(1); 2]).split(inner);
let areas = vertical.iter().flat_map(|area| {
Layout::horizontal([Constraint::Ratio(1, 8); 8])
.split(*area)
.to_vec()
});
for (bg, area) in NAMED_COLORS.into_iter().zip(areas) {
let color_name = bg.to_string();
let paragraph = Paragraph::new(color_name).fg(fg).bg(bg);
frame.render_widget(paragraph, area);
}
}
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::vertical([
Constraint::Length(1), // 0 - 15
Constraint::Length(1), // blank
Constraint::Min(6), // 16 - 123
Constraint::Length(1), // blank
Constraint::Min(6), // 124 - 231
Constraint::Length(1), // blank
])
.split(inner);
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
let color_layout = Layout::horizontal([Constraint::Length(5); 16]).split(layout[0]);
for i in 0..16 {
let color = Color::Indexed(i);
let color_index = format!("{i:0>2}");
let bg = if i < 1 { Color::DarkGray } else { Color::Black };
let paragraph = Paragraph::new(Line::from(vec![
color_index.fg(color).bg(bg),
"██".bg(color).fg(color),
]));
frame.render_widget(paragraph, color_layout[i as usize]);
}
// 16 17 18 19 20 21 52 53 54 55 56 57 88 89 90 91 92 93
// 22 23 24 25 26 27 58 59 60 61 62 63 94 95 96 97 98 99
// 28 29 30 31 32 33 64 65 66 67 68 69 100 101 102 103 104 105
// 34 35 36 37 38 39 70 71 72 73 74 75 106 107 108 109 110 111
// 40 41 42 43 44 45 76 77 78 79 80 81 112 113 114 115 116 117
// 46 47 48 49 50 51 82 83 84 85 86 87 118 119 120 121 122 123
//
// 124 125 126 127 128 129 160 161 162 163 164 165 196 197 198 199 200 201
// 130 131 132 133 134 135 166 167 168 169 170 171 202 203 204 205 206 207
// 136 137 138 139 140 141 172 173 174 175 176 177 208 209 210 211 212 213
// 142 143 144 145 146 147 178 179 180 181 182 183 214 215 216 217 218 219
// 148 149 150 151 152 153 184 185 186 187 188 189 220 221 222 223 224 225
// 154 155 156 157 158 159 190 191 192 193 194 195 226 227 228 229 230 231
// the above looks complex but it's so the colors are grouped into blocks that display nicely
let index_layout = [layout[2], layout[4]]
.iter()
// two rows of 3 columns
.flat_map(|area| {
Layout::horizontal([Constraint::Length(27); 3])
.split(*area)
.to_vec()
})
// each with 6 rows
.flat_map(|area| {
Layout::vertical([Constraint::Length(1); 6])
.split(area)
.to_vec()
})
// each with 6 columns
.flat_map(|area| {
Layout::horizontal([Constraint::Min(4); 6])
.split(area)
.to_vec()
})
.collect_vec();
for i in 16..=231 {
let color = Color::Indexed(i);
let color_index = format!("{i:0>3}");
let paragraph = Paragraph::new(Line::from(vec![
color_index.fg(color).bg(Color::Reset),
".".bg(color).fg(color),
// There's a bug in VHS that seems to bleed backgrounds into the next
// character. This is a workaround to make the bug less obvious.
"███".reversed(),
]));
frame.render_widget(paragraph, index_layout[i as usize - 16]);
}
}
fn title_block(title: String) -> Block<'static> {
Block::new()
.borders(Borders::TOP)
.title_alignment(Alignment::Center)
.border_style(Style::new().dark_gray())
.title_style(Style::new().reset())
.title(title)
}
fn render_indexed_grayscale(frame: &mut Frame, area: Rect) {
let layout = Layout::vertical([
Constraint::Length(1), // 232 - 243
Constraint::Length(1), // 244 - 255
])
.split(area)
.iter()
.flat_map(|area| {
Layout::horizontal([Constraint::Length(6); 12])
.split(*area)
.to_vec()
})
.collect_vec();
for i in 232..=255 {
let color = Color::Indexed(i);
let color_index = format!("{i:0>3}");
// make the dark colors easier to read
let bg = if i < 244 { Color::Gray } else { Color::Black };
let paragraph = Paragraph::new(Line::from(vec![
color_index.fg(color).bg(bg),
"██".bg(color).fg(color),
// There's a bug in VHS that seems to bleed backgrounds into the next
// character. This is a workaround to make the bug less obvious.
"███████".reversed(),
]));
frame.render_widget(paragraph, layout[i as usize - 232]);
}
}

256
examples/colors_rgb.rs Normal file
View File

@@ -0,0 +1,256 @@
//! # [Ratatui] `Colors_RGB` example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
// 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.
//
// This example also demonstrates how implementing the Widget trait on a mutable reference
// allows the widget to update its state while it is being rendered. This allows the fps
// widget to update the fps calculation and the colors widget to update a cached version of
// the colors to render instead of recalculating them every frame.
//
// This is an alternative to using the `StatefulWidget` trait and a separate state struct. It
// is useful when the state is only used by the widget and doesn't need to be shared with
// other widgets.
use std::time::{Duration, Instant};
use color_eyre::Result;
use palette::{convert::FromColorUnclamped, Okhsv, Srgb};
use ratatui::{
buffer::Buffer,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Layout, Position, Rect},
style::Color,
text::Text,
widgets::Widget,
DefaultTerminal,
};
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::default().run(terminal);
ratatui::restore();
app_result
}
#[derive(Debug, Default)]
struct App {
/// The current state of the app (running or quit)
state: AppState,
/// A widget that displays the current frames per second
fps_widget: FpsWidget,
/// A widget that displays the full range of RGB colors that can be displayed in the terminal.
colors_widget: ColorsWidget,
}
#[derive(Debug, Default, PartialEq, Eq)]
enum AppState {
/// The app is running
#[default]
Running,
/// The user has requested the app to quit
Quit,
}
/// A widget that displays the current frames per second
#[derive(Debug)]
struct FpsWidget {
/// The number of elapsed frames that have passed - used to calculate the fps
frame_count: usize,
/// The last instant that the fps was calculated
last_instant: Instant,
/// The current frames per second
fps: Option<f32>,
}
/// A widget that displays the full range of RGB colors that can be displayed in the terminal.
///
/// This widget is animated and will change colors over time.
#[derive(Debug, Default)]
struct ColorsWidget {
/// The colors to render - should be double the height of the area as we render two rows of
/// pixels for each row of the widget using the half block character. This is computed any time
/// the size of the widget changes.
colors: Vec<Vec<Color>>,
/// the number of elapsed frames that have passed - used to animate the colors by shifting the
/// x index by the frame number
frame_count: usize,
}
impl App {
/// Run the app
///
/// This is the main event loop for the app.
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
while self.is_running() {
terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?;
self.handle_events()?;
}
Ok(())
}
const fn is_running(&self) -> bool {
matches!(self.state, AppState::Running)
}
/// Handle any events that have occurred since the last time the app was rendered.
///
/// Currently, this only handles the q key to quit the app.
fn handle_events(&mut self) -> Result<()> {
// Ensure that the app only blocks for a period that allows the app to render at
// approximately 60 FPS (this doesn't account for the time to render the frame, and will
// also update the app immediately any time an event occurs)
let timeout = Duration::from_secs_f32(1.0 / 60.0);
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
self.state = AppState::Quit;
};
}
}
Ok(())
}
}
/// Implement the Widget trait for &mut App so that it can be rendered
///
/// This is implemented on a mutable reference so that the app can update its state while it is
/// being rendered. This allows the fps widget to update the fps calculation and the colors widget
/// to update the colors to render.
impl Widget for &mut App {
fn render(self, area: Rect, buf: &mut Buffer) {
use Constraint::{Length, Min};
let [top, colors] = Layout::vertical([Length(1), Min(0)]).areas(area);
let [title, fps] = Layout::horizontal([Min(0), Length(8)]).areas(top);
Text::from("colors_rgb example. Press q to quit")
.centered()
.render(title, buf);
self.fps_widget.render(fps, buf);
self.colors_widget.render(colors, buf);
}
}
/// Default impl for `FpsWidget`
///
/// Manual impl is required because we need to initialize the `last_instant` field to the current
/// instant.
impl Default for FpsWidget {
fn default() -> Self {
Self {
frame_count: 0,
last_instant: Instant::now(),
fps: None,
}
}
}
/// Widget impl for `FpsWidget`
///
/// This is implemented on a mutable reference so that we can update the frame count and fps
/// calculation while rendering.
impl Widget for &mut FpsWidget {
fn render(self, area: Rect, buf: &mut Buffer) {
self.calculate_fps();
if let Some(fps) = self.fps {
let text = format!("{fps:.1} fps");
Text::from(text).render(area, buf);
}
}
}
impl FpsWidget {
/// Update the fps calculation.
///
/// This updates the fps once a second, but only if the widget has rendered at least 2 frames
/// since the last calculation. This avoids noise in the fps calculation when rendering on slow
/// machines that can't render at least 2 frames per second.
#[allow(clippy::cast_precision_loss)]
fn calculate_fps(&mut self) {
self.frame_count += 1;
let elapsed = self.last_instant.elapsed();
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();
}
}
}
/// Widget impl for `ColorsWidget`
///
/// This is implemented on a mutable reference so that we can update the frame count and store a
/// cached version of the colors to render instead of recalculating them every frame.
impl Widget for &mut ColorsWidget {
/// Render the widget
fn render(self, area: Rect, buf: &mut Buffer) {
self.setup_colors(area);
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() {
// render a half block character for each row of pixels with the foreground color
// set to the color of the pixel and the background color set to the color of the
// pixel below it
let fg = colors[yi * 2][xi];
let bg = colors[yi * 2 + 1][xi];
buf[Position::new(x, y)].set_char('▀').set_fg(fg).set_bg(bg);
}
}
self.frame_count += 1;
}
}
impl ColorsWidget {
/// Setup the colors to render.
///
/// This is called once per frame to setup the colors to render. It caches the colors so that
/// they don't need to be recalculated every frame.
#[allow(clippy::cast_precision_loss)]
fn setup_colors(&mut self, size: Rect) {
let Rect { width, height, .. } = size;
// double the height because each screen row has two rows of half block pixels
let height = height as usize * 2;
let width = width as usize;
// only update the colors if the size has changed since the last time we rendered
if self.colors.len() == height && self.colors[0].len() == width {
return;
}
self.colors = Vec::with_capacity(height);
for y in 0..height {
let mut row = Vec::with_capacity(width);
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);
}
}
}

View File

@@ -0,0 +1,606 @@
//! # [Ratatui] Constraint explorer example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use color_eyre::Result;
use itertools::Itertools;
use ratatui::{
buffer::Buffer,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
Flex, Layout, Rect,
},
style::{
palette::tailwind::{BLUE, SKY, SLATE, STONE},
Color, Style, Stylize,
},
symbols::{self, line},
text::{Line, Span, Text},
widgets::{Block, Paragraph, Widget, Wrap},
DefaultTerminal,
};
use strum::{Display, EnumIter, FromRepr};
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::default().run(terminal);
ratatui::restore();
app_result
}
#[derive(Default)]
struct App {
mode: AppMode,
spacing: u16,
constraints: Vec<Constraint>,
selected_index: usize,
value: u16,
}
#[derive(Debug, Default, PartialEq, Eq)]
enum AppMode {
#[default]
Running,
Quit,
}
/// A variant of [`Constraint`] that can be rendered as a tab.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, EnumIter, FromRepr, Display)]
enum ConstraintName {
#[default]
Length,
Percentage,
Ratio,
Min,
Max,
Fill,
}
/// A widget that renders a [`Constraint`] as a block. E.g.:
/// ```plain
/// ┌──────────────┐
/// │ Length(16) │
/// │ 16px │
/// └──────────────┘
/// ```
struct ConstraintBlock {
constraint: Constraint,
legend: bool,
selected: bool,
}
/// A widget that renders a spacer with a label indicating the width of the spacer. E.g.:
///
/// ```plain
/// ┌ ┐
/// 8 px
/// └ ┘
/// ```
struct SpacerBlock;
// App behaviour
impl App {
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
self.insert_test_defaults();
while self.is_running() {
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
self.handle_events()?;
}
Ok(())
}
// TODO remove these - these are just for testing
fn insert_test_defaults(&mut self) {
self.constraints = vec![
Constraint::Length(20),
Constraint::Length(20),
Constraint::Length(20),
];
self.value = 20;
}
fn is_running(&self) -> bool {
self.mode == AppMode::Running
}
fn handle_events(&mut self) -> Result<()> {
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
KeyCode::Char('q') | KeyCode::Esc => self.exit(),
KeyCode::Char('1') => self.swap_constraint(ConstraintName::Min),
KeyCode::Char('2') => self.swap_constraint(ConstraintName::Max),
KeyCode::Char('3') => self.swap_constraint(ConstraintName::Length),
KeyCode::Char('4') => self.swap_constraint(ConstraintName::Percentage),
KeyCode::Char('5') => self.swap_constraint(ConstraintName::Ratio),
KeyCode::Char('6') => self.swap_constraint(ConstraintName::Fill),
KeyCode::Char('+') => self.increment_spacing(),
KeyCode::Char('-') => self.decrement_spacing(),
KeyCode::Char('x') => self.delete_block(),
KeyCode::Char('a') => self.insert_block(),
KeyCode::Char('k') | KeyCode::Up => self.increment_value(),
KeyCode::Char('j') | KeyCode::Down => self.decrement_value(),
KeyCode::Char('h') | KeyCode::Left => self.prev_block(),
KeyCode::Char('l') | KeyCode::Right => self.next_block(),
_ => {}
},
_ => {}
}
Ok(())
}
fn increment_value(&mut self) {
let Some(constraint) = self.constraints.get_mut(self.selected_index) else {
return;
};
match constraint {
Constraint::Length(v)
| Constraint::Min(v)
| Constraint::Max(v)
| Constraint::Fill(v)
| Constraint::Percentage(v) => *v = v.saturating_add(1),
Constraint::Ratio(_n, d) => *d = d.saturating_add(1),
};
}
fn decrement_value(&mut self) {
let Some(constraint) = self.constraints.get_mut(self.selected_index) else {
return;
};
match constraint {
Constraint::Length(v)
| Constraint::Min(v)
| Constraint::Max(v)
| Constraint::Fill(v)
| Constraint::Percentage(v) => *v = v.saturating_sub(1),
Constraint::Ratio(_n, d) => *d = d.saturating_sub(1),
};
}
/// select the next block with wrap around
fn next_block(&mut self) {
if self.constraints.is_empty() {
return;
}
let len = self.constraints.len();
self.selected_index = (self.selected_index + 1) % len;
}
/// select the previous block with wrap around
fn prev_block(&mut self) {
if self.constraints.is_empty() {
return;
}
let len = self.constraints.len();
self.selected_index = (self.selected_index + self.constraints.len() - 1) % len;
}
/// delete the selected block
fn delete_block(&mut self) {
if self.constraints.is_empty() {
return;
}
self.constraints.remove(self.selected_index);
self.selected_index = self.selected_index.saturating_sub(1);
}
/// insert a block after the selected block
fn insert_block(&mut self) {
let index = self
.selected_index
.saturating_add(1)
.min(self.constraints.len());
let constraint = Constraint::Length(self.value);
self.constraints.insert(index, constraint);
self.selected_index = index;
}
fn increment_spacing(&mut self) {
self.spacing = self.spacing.saturating_add(1);
}
fn decrement_spacing(&mut self) {
self.spacing = self.spacing.saturating_sub(1);
}
fn exit(&mut self) {
self.mode = AppMode::Quit;
}
fn swap_constraint(&mut self, name: ConstraintName) {
if self.constraints.is_empty() {
return;
}
let constraint = match name {
ConstraintName::Length => Length(self.value),
ConstraintName::Percentage => Percentage(self.value),
ConstraintName::Min => Min(self.value),
ConstraintName::Max => Max(self.value),
ConstraintName::Fill => Fill(self.value),
ConstraintName::Ratio => Ratio(1, u32::from(self.value) / 4), // for balance
};
self.constraints[self.selected_index] = constraint;
}
}
impl From<Constraint> for ConstraintName {
fn from(constraint: Constraint) -> Self {
match constraint {
Length(_) => Self::Length,
Percentage(_) => Self::Percentage,
Ratio(_, _) => Self::Ratio,
Min(_) => Self::Min,
Max(_) => Self::Max,
Fill(_) => Self::Fill,
}
}
}
impl Widget for &App {
fn render(self, area: Rect, buf: &mut Buffer) {
let [header_area, instructions_area, swap_legend_area, _, blocks_area] =
Layout::vertical([
Length(2), // header
Length(2), // instructions
Length(1), // swap key legend
Length(1), // gap
Fill(1), // blocks
])
.areas(area);
App::header().render(header_area, buf);
App::instructions().render(instructions_area, buf);
App::swap_legend().render(swap_legend_area, buf);
self.render_layout_blocks(blocks_area, buf);
}
}
// App rendering
impl App {
const HEADER_COLOR: Color = SLATE.c200;
const TEXT_COLOR: Color = SLATE.c400;
const AXIS_COLOR: Color = SLATE.c500;
fn header() -> impl Widget {
let text = "Constraint Explorer";
text.bold().fg(Self::HEADER_COLOR).into_centered_line()
}
fn instructions() -> impl Widget {
let text = "◄ ►: select, ▲ ▼: edit, 1-6: swap, a: add, x: delete, q: quit, + -: spacing";
Paragraph::new(text)
.fg(Self::TEXT_COLOR)
.centered()
.wrap(Wrap { trim: false })
}
fn swap_legend() -> impl Widget {
#[allow(unstable_name_collisions)]
Paragraph::new(
Line::from(
[
ConstraintName::Min,
ConstraintName::Max,
ConstraintName::Length,
ConstraintName::Percentage,
ConstraintName::Ratio,
ConstraintName::Fill,
]
.iter()
.enumerate()
.map(|(i, name)| {
format!(" {i}: {name} ", i = i + 1)
.fg(SLATE.c200)
.bg(name.color())
})
.intersperse(Span::from(" "))
.collect_vec(),
)
.centered(),
)
.wrap(Wrap { trim: false })
}
/// A bar like `<----- 80 px (gap: 2 px) ----->`
///
/// Only shows the gap when spacing is not zero
fn axis(&self, width: u16) -> impl Widget {
let label = if self.spacing != 0 {
format!("{} px (gap: {} px)", width, self.spacing)
} else {
format!("{width} px")
};
let bar_width = width.saturating_sub(2) as usize; // we want to `<` and `>` at the ends
let width_bar = format!("<{label:-^bar_width$}>");
Paragraph::new(width_bar).fg(Self::AXIS_COLOR).centered()
}
fn render_layout_blocks(&self, area: Rect, buf: &mut Buffer) {
let [user_constraints, area] = Layout::vertical([Length(3), Fill(1)])
.spacing(1)
.areas(area);
self.render_user_constraints_legend(user_constraints, buf);
let [start, center, end, space_around, space_between] =
Layout::vertical([Length(7); 5]).areas(area);
self.render_layout_block(Flex::Start, start, buf);
self.render_layout_block(Flex::Center, center, buf);
self.render_layout_block(Flex::End, end, buf);
self.render_layout_block(Flex::SpaceAround, space_around, buf);
self.render_layout_block(Flex::SpaceBetween, space_between, buf);
}
fn render_user_constraints_legend(&self, area: Rect, buf: &mut Buffer) {
let constraints = self.constraints.iter().map(|_| Constraint::Fill(1));
let blocks = Layout::horizontal(constraints).split(area);
for (i, (area, constraint)) in blocks.iter().zip(self.constraints.iter()).enumerate() {
let selected = self.selected_index == i;
ConstraintBlock::new(*constraint, selected, true).render(*area, buf);
}
}
fn render_layout_block(&self, flex: Flex, area: Rect, buf: &mut Buffer) {
let [label_area, axis_area, blocks_area] =
Layout::vertical([Length(1), Max(1), Length(4)]).areas(area);
if label_area.height > 0 {
format!("Flex::{flex:?}").bold().render(label_area, buf);
}
self.axis(area.width).render(axis_area, buf);
let (blocks, spacers) = Layout::horizontal(&self.constraints)
.flex(flex)
.spacing(self.spacing)
.split_with_spacers(blocks_area);
for (i, (area, constraint)) in blocks.iter().zip(self.constraints.iter()).enumerate() {
let selected = self.selected_index == i;
ConstraintBlock::new(*constraint, selected, false).render(*area, buf);
}
for area in spacers.iter() {
SpacerBlock.render(*area, buf);
}
}
}
impl Widget for ConstraintBlock {
fn render(self, area: Rect, buf: &mut Buffer) {
match area.height {
1 => self.render_1px(area, buf),
2 => self.render_2px(area, buf),
_ => self.render_4px(area, buf),
}
}
}
impl ConstraintBlock {
const TEXT_COLOR: Color = SLATE.c200;
const fn new(constraint: Constraint, selected: bool, legend: bool) -> Self {
Self {
constraint,
legend,
selected,
}
}
fn label(&self, width: u16) -> String {
let long_width = format!("{width} px");
let short_width = format!("{width}");
// border takes up 2 columns
let available_space = width.saturating_sub(2) as usize;
let width_label = if long_width.len() < available_space {
long_width
} else if short_width.len() < available_space {
short_width
} else {
String::new()
};
format!("{}\n{}", self.constraint, width_label)
}
fn render_1px(&self, area: Rect, buf: &mut Buffer) {
let lighter_color = ConstraintName::from(self.constraint).lighter_color();
let main_color = ConstraintName::from(self.constraint).color();
let selected_color = if self.selected {
lighter_color
} else {
main_color
};
Block::new()
.fg(Self::TEXT_COLOR)
.bg(selected_color)
.render(area, buf);
}
fn render_2px(&self, area: Rect, buf: &mut Buffer) {
let lighter_color = ConstraintName::from(self.constraint).lighter_color();
let main_color = ConstraintName::from(self.constraint).color();
let selected_color = if self.selected {
lighter_color
} else {
main_color
};
Block::bordered()
.border_set(symbols::border::QUADRANT_OUTSIDE)
.border_style(Style::reset().fg(selected_color).reversed())
.render(area, buf);
}
fn render_4px(&self, area: Rect, buf: &mut Buffer) {
let lighter_color = ConstraintName::from(self.constraint).lighter_color();
let main_color = ConstraintName::from(self.constraint).color();
let selected_color = if self.selected {
lighter_color
} else {
main_color
};
let color = if self.legend {
selected_color
} else {
main_color
};
let label = self.label(area.width);
let block = Block::bordered()
.border_set(symbols::border::QUADRANT_OUTSIDE)
.border_style(Style::reset().fg(color).reversed())
.fg(Self::TEXT_COLOR)
.bg(color);
Paragraph::new(label)
.centered()
.fg(Self::TEXT_COLOR)
.bg(color)
.block(block)
.render(area, buf);
if !self.legend {
let border_color = if self.selected {
lighter_color
} else {
main_color
};
if let Some(last_row) = area.rows().last() {
buf.set_style(last_row, border_color);
}
}
}
}
impl Widget for SpacerBlock {
fn render(self, area: Rect, buf: &mut Buffer) {
match area.height {
1 => (),
2 => Self::render_2px(area, buf),
3 => Self::render_3px(area, buf),
_ => Self::render_4px(area, buf),
}
}
}
impl SpacerBlock {
const TEXT_COLOR: Color = SLATE.c500;
const BORDER_COLOR: Color = SLATE.c600;
/// A block with a corner borders
fn block() -> impl Widget {
let corners_only = symbols::border::Set {
top_left: line::NORMAL.top_left,
top_right: line::NORMAL.top_right,
bottom_left: line::NORMAL.bottom_left,
bottom_right: line::NORMAL.bottom_right,
vertical_left: " ",
vertical_right: " ",
horizontal_top: " ",
horizontal_bottom: " ",
};
Block::bordered()
.border_set(corners_only)
.border_style(Self::BORDER_COLOR)
}
/// A vertical line used if there is not enough space to render the block
fn line() -> impl Widget {
Paragraph::new(Text::from(vec![
Line::from(""),
Line::from(""),
Line::from(""),
Line::from(""),
]))
.style(Self::BORDER_COLOR)
}
/// A label that says "Spacer" if there is enough space
fn spacer_label(width: u16) -> impl Widget {
let label = if width >= 6 { "Spacer" } else { "" };
label.fg(Self::TEXT_COLOR).into_centered_line()
}
/// A label that says "8 px" if there is enough space
fn label(width: u16) -> impl Widget {
let long_label = format!("{width} px");
let short_label = format!("{width}");
let label = if long_label.len() < width as usize {
long_label
} else if short_label.len() < width as usize {
short_label
} else {
String::new()
};
Line::styled(label, Self::TEXT_COLOR).centered()
}
fn render_2px(area: Rect, buf: &mut Buffer) {
if area.width > 1 {
Self::block().render(area, buf);
} else {
Self::line().render(area, buf);
}
}
fn render_3px(area: Rect, buf: &mut Buffer) {
if area.width > 1 {
Self::block().render(area, buf);
} else {
Self::line().render(area, buf);
}
let row = area.rows().nth(1).unwrap_or_default();
Self::spacer_label(area.width).render(row, buf);
}
fn render_4px(area: Rect, buf: &mut Buffer) {
if area.width > 1 {
Self::block().render(area, buf);
} else {
Self::line().render(area, buf);
}
let row = area.rows().nth(1).unwrap_or_default();
Self::spacer_label(area.width).render(row, buf);
let row = area.rows().nth(2).unwrap_or_default();
Self::label(area.width).render(row, buf);
}
}
impl ConstraintName {
const fn color(self) -> Color {
match self {
Self::Length => SLATE.c700,
Self::Percentage => SLATE.c800,
Self::Ratio => SLATE.c900,
Self::Fill => SLATE.c950,
Self::Min => BLUE.c800,
Self::Max => BLUE.c900,
}
}
const fn lighter_color(self) -> Color {
match self {
Self::Length => STONE.c500,
Self::Percentage => STONE.c600,
Self::Ratio => STONE.c700,
Self::Fill => STONE.c800,
Self::Min => SKY.c600,
Self::Max => SKY.c700,
}
}
}

405
examples/constraints.rs Normal file
View File

@@ -0,0 +1,405 @@
//! # [Ratatui] Constraints example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use color_eyre::Result;
use ratatui::{
buffer::Buffer,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
Layout, Rect,
},
style::{palette::tailwind, Color, Modifier, Style, Stylize},
symbols,
text::Line,
widgets::{
Block, Padding, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget,
Tabs, Widget,
},
DefaultTerminal,
};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
const SPACER_HEIGHT: u16 = 0;
const ILLUSTRATION_HEIGHT: u16 = 4;
const EXAMPLE_HEIGHT: u16 = ILLUSTRATION_HEIGHT + SPACER_HEIGHT;
// priority 2
const MIN_COLOR: Color = tailwind::BLUE.c900;
const MAX_COLOR: Color = tailwind::BLUE.c800;
// priority 3
const LENGTH_COLOR: Color = tailwind::SLATE.c700;
const PERCENTAGE_COLOR: Color = tailwind::SLATE.c800;
const RATIO_COLOR: Color = tailwind::SLATE.c900;
// priority 4
const FILL_COLOR: Color = tailwind::SLATE.c950;
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::default().run(terminal);
ratatui::restore();
app_result
}
#[derive(Default, Clone, Copy)]
struct App {
selected_tab: SelectedTab,
scroll_offset: u16,
max_scroll_offset: u16,
state: AppState,
}
/// Tabs for the different examples
///
/// The order of the variants is the order in which they are displayed.
#[derive(Default, Debug, Copy, Clone, Display, FromRepr, EnumIter, PartialEq, Eq)]
enum SelectedTab {
#[default]
Min,
Max,
Length,
Percentage,
Ratio,
Fill,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum AppState {
#[default]
Running,
Quit,
}
impl App {
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
self.update_max_scroll_offset();
while self.is_running() {
terminal.draw(|frame| frame.render_widget(self, frame.area()))?;
self.handle_events()?;
}
Ok(())
}
fn update_max_scroll_offset(&mut self) {
self.max_scroll_offset = (self.selected_tab.get_example_count() - 1) * EXAMPLE_HEIGHT;
}
fn is_running(self) -> bool {
self.state == AppState::Running
}
fn handle_events(&mut self) -> Result<()> {
if let Event::Key(key) = event::read()? {
if key.kind != KeyEventKind::Press {
return Ok(());
}
match key.code {
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
KeyCode::Char('l') | KeyCode::Right => self.next(),
KeyCode::Char('h') | KeyCode::Left => self.previous(),
KeyCode::Char('j') | KeyCode::Down => self.down(),
KeyCode::Char('k') | KeyCode::Up => self.up(),
KeyCode::Char('g') | KeyCode::Home => self.top(),
KeyCode::Char('G') | KeyCode::End => self.bottom(),
_ => (),
}
}
Ok(())
}
fn quit(&mut self) {
self.state = AppState::Quit;
}
fn next(&mut self) {
self.selected_tab = self.selected_tab.next();
self.update_max_scroll_offset();
self.scroll_offset = 0;
}
fn previous(&mut self) {
self.selected_tab = self.selected_tab.previous();
self.update_max_scroll_offset();
self.scroll_offset = 0;
}
fn up(&mut self) {
self.scroll_offset = self.scroll_offset.saturating_sub(1);
}
fn down(&mut self) {
self.scroll_offset = self
.scroll_offset
.saturating_add(1)
.min(self.max_scroll_offset);
}
fn top(&mut self) {
self.scroll_offset = 0;
}
fn bottom(&mut self) {
self.scroll_offset = self.max_scroll_offset;
}
}
impl Widget for App {
fn render(self, area: Rect, buf: &mut Buffer) {
let [tabs, axis, demo] = Layout::vertical([Length(3), Length(3), Fill(0)]).areas(area);
self.render_tabs(tabs, buf);
Self::render_axis(axis, buf);
self.render_demo(demo, buf);
}
}
impl App {
fn render_tabs(self, area: Rect, buf: &mut Buffer) {
let titles = SelectedTab::iter().map(SelectedTab::to_tab_title);
let block = Block::new()
.title("Constraints ".bold())
.title(" Use h l or ◄ ► to change tab and j k or ▲ ▼ to scroll");
Tabs::new(titles)
.block(block)
.highlight_style(Modifier::REVERSED)
.select(self.selected_tab as usize)
.padding("", "")
.divider(" ")
.render(area, buf);
}
fn render_axis(area: Rect, buf: &mut Buffer) {
let width = area.width as usize;
// a bar like `<----- 80 px ----->`
let width_label = format!("{width} px");
let width_bar = format!(
"<{width_label:-^width$}>",
width = width - width_label.len() / 2
);
Paragraph::new(width_bar.dark_gray())
.centered()
.block(Block::new().padding(Padding {
left: 0,
right: 0,
top: 1,
bottom: 0,
}))
.render(area, buf);
}
/// Render the demo content
///
/// This function renders the demo content into a separate buffer and then splices the buffer
/// into the main buffer. This is done to make it possible to handle scrolling easily.
#[allow(clippy::cast_possible_truncation)]
fn render_demo(self, area: Rect, buf: &mut Buffer) {
// render demo content into a separate buffer so all examples fit we add an extra
// area.height to make sure the last example is fully visible even when the scroll offset is
// at the max
let height = self.selected_tab.get_example_count() * EXAMPLE_HEIGHT;
let demo_area = Rect::new(0, 0, area.width, height + area.height);
let mut demo_buf = Buffer::empty(demo_area);
let scrollbar_needed = self.scroll_offset != 0 || height > area.height;
let content_area = if scrollbar_needed {
Rect {
width: demo_area.width - 1,
..demo_area
}
} else {
demo_area
};
self.selected_tab.render(content_area, &mut demo_buf);
let visible_content = demo_buf
.content
.into_iter()
.skip((demo_area.width * self.scroll_offset) as usize)
.take(area.area() as usize);
for (i, cell) in visible_content.enumerate() {
let x = i as u16 % area.width;
let y = i as u16 / area.width;
buf[(area.x + x, area.y + y)] = cell;
}
if scrollbar_needed {
let mut state = ScrollbarState::new(self.max_scroll_offset as usize)
.position(self.scroll_offset as usize);
Scrollbar::new(ScrollbarOrientation::VerticalRight).render(area, buf, &mut state);
}
}
}
impl SelectedTab {
/// Get the previous tab, if there is no previous tab return the current tab.
fn previous(self) -> Self {
let current_index: usize = self as usize;
let previous_index = current_index.saturating_sub(1);
Self::from_repr(previous_index).unwrap_or(self)
}
/// Get the next tab, if there is no next tab return the current tab.
fn next(self) -> Self {
let current_index = self as usize;
let next_index = current_index.saturating_add(1);
Self::from_repr(next_index).unwrap_or(self)
}
const fn get_example_count(self) -> u16 {
#[allow(clippy::match_same_arms)]
match self {
Self::Length => 4,
Self::Percentage => 5,
Self::Ratio => 4,
Self::Fill => 2,
Self::Min => 5,
Self::Max => 5,
}
}
fn to_tab_title(value: Self) -> Line<'static> {
let text = format!(" {value} ");
let color = match value {
Self::Length => LENGTH_COLOR,
Self::Percentage => PERCENTAGE_COLOR,
Self::Ratio => RATIO_COLOR,
Self::Fill => FILL_COLOR,
Self::Min => MIN_COLOR,
Self::Max => MAX_COLOR,
};
text.fg(tailwind::SLATE.c200).bg(color).into()
}
}
impl Widget for SelectedTab {
fn render(self, area: Rect, buf: &mut Buffer) {
match self {
Self::Length => Self::render_length_example(area, buf),
Self::Percentage => Self::render_percentage_example(area, buf),
Self::Ratio => Self::render_ratio_example(area, buf),
Self::Fill => Self::render_fill_example(area, buf),
Self::Min => Self::render_min_example(area, buf),
Self::Max => Self::render_max_example(area, buf),
}
}
}
impl SelectedTab {
fn render_length_example(area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, _] =
Layout::vertical([Length(EXAMPLE_HEIGHT); 4]).areas(area);
Example::new(&[Length(20), Length(20)]).render(example1, buf);
Example::new(&[Length(20), Min(20)]).render(example2, buf);
Example::new(&[Length(20), Max(20)]).render(example3, buf);
}
fn render_percentage_example(area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, example5, _] =
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
Example::new(&[Percentage(75), Fill(0)]).render(example1, buf);
Example::new(&[Percentage(25), Fill(0)]).render(example2, buf);
Example::new(&[Percentage(50), Min(20)]).render(example3, buf);
Example::new(&[Percentage(0), Max(0)]).render(example4, buf);
Example::new(&[Percentage(0), Fill(0)]).render(example5, buf);
}
fn render_ratio_example(area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, _] =
Layout::vertical([Length(EXAMPLE_HEIGHT); 5]).areas(area);
Example::new(&[Ratio(1, 2); 2]).render(example1, buf);
Example::new(&[Ratio(1, 4); 4]).render(example2, buf);
Example::new(&[Ratio(1, 2), Ratio(1, 3), Ratio(1, 4)]).render(example3, buf);
Example::new(&[Ratio(1, 2), Percentage(25), Length(10)]).render(example4, buf);
}
fn render_fill_example(area: Rect, buf: &mut Buffer) {
let [example1, example2, _] = Layout::vertical([Length(EXAMPLE_HEIGHT); 3]).areas(area);
Example::new(&[Fill(1), Fill(2), Fill(3)]).render(example1, buf);
Example::new(&[Fill(1), Percentage(50), Fill(1)]).render(example2, buf);
}
fn render_min_example(area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, example5, _] =
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
Example::new(&[Percentage(100), Min(0)]).render(example1, buf);
Example::new(&[Percentage(100), Min(20)]).render(example2, buf);
Example::new(&[Percentage(100), Min(40)]).render(example3, buf);
Example::new(&[Percentage(100), Min(60)]).render(example4, buf);
Example::new(&[Percentage(100), Min(80)]).render(example5, buf);
}
fn render_max_example(area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, example5, _] =
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
Example::new(&[Percentage(0), Max(0)]).render(example1, buf);
Example::new(&[Percentage(0), Max(20)]).render(example2, buf);
Example::new(&[Percentage(0), Max(40)]).render(example3, buf);
Example::new(&[Percentage(0), Max(60)]).render(example4, buf);
Example::new(&[Percentage(0), Max(80)]).render(example5, buf);
}
}
struct Example {
constraints: Vec<Constraint>,
}
impl Example {
fn new(constraints: &[Constraint]) -> Self {
Self {
constraints: constraints.into(),
}
}
}
impl Widget for Example {
fn render(self, area: Rect, buf: &mut Buffer) {
let [area, _] =
Layout::vertical([Length(ILLUSTRATION_HEIGHT), Length(SPACER_HEIGHT)]).areas(area);
let blocks = Layout::horizontal(&self.constraints).split(area);
for (block, constraint) in blocks.iter().zip(&self.constraints) {
Self::illustration(*constraint, block.width).render(*block, buf);
}
}
}
impl Example {
fn illustration(constraint: Constraint, width: u16) -> impl Widget {
let color = match constraint {
Constraint::Length(_) => LENGTH_COLOR,
Constraint::Percentage(_) => PERCENTAGE_COLOR,
Constraint::Ratio(_, _) => RATIO_COLOR,
Constraint::Fill(_) => FILL_COLOR,
Constraint::Min(_) => MIN_COLOR,
Constraint::Max(_) => MAX_COLOR,
};
let fg = Color::White;
let title = format!("{constraint}");
let content = format!("{width} px");
let text = format!("{title}\n{content}");
let block = Block::bordered()
.border_set(symbols::border::QUADRANT_OUTSIDE)
.border_style(Style::reset().fg(color).reversed())
.style(Style::default().fg(fg).bg(color));
Paragraph::new(text).centered().block(block)
}
}

View File

@@ -1,102 +0,0 @@
#[allow(dead_code)]
mod demo;
#[allow(dead_code)]
mod util;
use crate::demo::{ui, App};
use argh::FromArgs;
use crossterm::{
event::{self, Event as CEvent, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{
error::Error,
io::{stdout, Write},
sync::mpsc,
thread,
time::{Duration, Instant},
};
use tui::{backend::CrosstermBackend, Terminal};
enum Event<I> {
Input(I),
Tick,
}
/// Crossterm demo
#[derive(Debug, FromArgs)]
struct Cli {
/// time in ms between two ticks.
#[argh(option, default = "250")]
tick_rate: u64,
/// whether unicode symbols are used to improve the overall look of the app
#[argh(option, default = "true")]
enhanced_graphics: bool,
}
fn main() -> Result<(), Box<dyn Error>> {
let cli: Cli = argh::from_env();
enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
// Setup input handling
let (tx, rx) = mpsc::channel();
let tick_rate = Duration::from_millis(cli.tick_rate);
thread::spawn(move || {
let mut last_tick = Instant::now();
loop {
// poll for tick rate duration, if no events, sent tick event.
if event::poll(tick_rate - last_tick.elapsed()).unwrap() {
if let CEvent::Key(key) = event::read().unwrap() {
tx.send(Event::Input(key)).unwrap();
}
}
if last_tick.elapsed() >= tick_rate {
tx.send(Event::Tick).unwrap();
last_tick = Instant::now();
}
}
});
let mut app = App::new("Crossterm Demo", cli.enhanced_graphics);
terminal.clear()?;
loop {
terminal.draw(|mut f| ui::draw(&mut f, &mut app))?;
match rx.recv()? {
Event::Input(event) => match event.code {
KeyCode::Char('q') => {
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
break;
}
KeyCode::Char(c) => app.on_key(c),
KeyCode::Left => app.on_left(),
KeyCode::Up => app.on_up(),
KeyCode::Right => app.on_right(),
KeyCode::Down => app.on_down(),
_ => {}
},
Event::Tick => {
app.on_tick();
}
}
if app.should_quit {
break;
}
}
Ok(())
}

View File

@@ -1,77 +0,0 @@
mod demo;
#[allow(dead_code)]
mod util;
use crate::demo::{ui, App};
use argh::FromArgs;
use easycurses;
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use tui::{backend::CursesBackend, Terminal};
/// Curses demo
#[derive(Debug, FromArgs)]
struct Cli {
/// time in ms between two ticks.
#[argh(option, default = "250")]
tick_rate: u64,
/// whether unicode symbols are used to improve the overall look of the app
#[argh(option, default = "true")]
enhanced_graphics: bool,
}
fn main() -> Result<(), Box<dyn Error>> {
let cli: Cli = argh::from_env();
let mut backend = CursesBackend::new().ok_or(io::Error::new(io::ErrorKind::Other, ""))?;
let curses = backend.get_curses_mut();
curses.set_echo(false);
curses.set_input_timeout(easycurses::TimeoutMode::WaitUpTo(50));
curses.set_input_mode(easycurses::InputMode::RawCharacter);
curses.set_keypad_enabled(true);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let mut app = App::new("Curses demo", cli.enhanced_graphics);
let mut last_tick = Instant::now();
let tick_rate = Duration::from_millis(cli.tick_rate);
loop {
terminal.draw(|mut f| ui::draw(&mut f, &mut app))?;
match terminal.backend_mut().get_curses_mut().get_input() {
Some(input) => {
match input {
easycurses::Input::Character(c) => {
app.on_key(c);
}
easycurses::Input::KeyUp => {
app.on_up();
}
easycurses::Input::KeyDown => {
app.on_down();
}
easycurses::Input::KeyLeft => {
app.on_left();
}
easycurses::Input::KeyRight => {
app.on_right();
}
_ => {}
};
}
_ => {}
};
terminal.backend_mut().get_curses_mut().flush_input();
if last_tick.elapsed() > tick_rate {
app.on_tick();
last_tick = Instant::now();
}
if app.should_quit {
break;
}
}
Ok(())
}

View File

@@ -1,62 +1,274 @@
#[allow(dead_code)]
mod util;
//! # [Ratatui] Custom Widget example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use crate::util::event::{Event, Events};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend, buffer::Buffer, layout::Rect, style::Style, widgets::Widget, Terminal,
use std::{io::stdout, ops::ControlFlow, time::Duration};
use color_eyre::Result;
use ratatui::{
buffer::Buffer,
crossterm::{
event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseButton, MouseEvent,
MouseEventKind,
},
execute,
},
layout::{Constraint, Layout, Rect},
style::{Color, Style},
text::Line,
widgets::{Paragraph, Widget},
DefaultTerminal, Frame,
};
struct Label<'a> {
text: &'a str,
}
impl<'a> Default for Label<'a> {
fn default() -> Label<'a> {
Label { text: "" }
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
execute!(stdout(), EnableMouseCapture)?;
let app_result = run(terminal);
ratatui::restore();
if let Err(err) = execute!(stdout(), DisableMouseCapture) {
eprintln!("Error disabling mouse capture: {err}");
}
app_result
}
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());
/// 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,
}
#[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) -> Self {
Button {
label: label.into(),
theme: BLUE,
state: State::Normal,
}
}
}
impl<'a> Label<'a> {
fn text(mut self, text: &'a str) -> Label<'a> {
self.text = text;
pub const fn theme(mut self, theme: Theme) -> Self {
self.theme = theme;
self
}
pub const fn state(mut self, state: State) -> Self {
self.state = state;
self
}
}
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
impl<'a> Widget for Button<'a> {
#[allow(clippy::cast_possible_truncation)]
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));
let events = Events::new();
// 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<'_> {
const 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),
}
}
}
fn run(mut terminal: DefaultTerminal) -> Result<()> {
let mut selected_button: usize = 0;
let mut button_states = [State::Selected, State::Normal, State::Normal];
loop {
terminal.draw(|mut f| {
let size = f.size();
let label = Label::default().text("Test");
f.render_widget(label, size);
})?;
match events.next()? {
Event::Input(key) => {
if key == Key::Char('q') {
terminal.draw(|frame| draw(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, &mut button_states, &mut selected_button).is_break() {
break;
}
}
_ => {}
Event::Mouse(mouse) => {
handle_mouse_event(mouse, &mut button_states, &mut selected_button);
}
_ => (),
}
}
Ok(())
}
fn draw(frame: &mut Frame, states: [State; 3]) {
let vertical = Layout::vertical([
Constraint::Length(1),
Constraint::Max(3),
Constraint::Length(1),
Constraint::Min(0), // ignore remaining space
]);
let [title, buttons, help, _] = vertical.areas(frame.area());
frame.render_widget(
Paragraph::new("Custom Widget Example (mouse enabled)"),
title,
);
render_buttons(frame, buttons, states);
frame.render_widget(Paragraph::new("←/→: select, Space: toggle, q: quit"), help);
}
fn render_buttons(frame: &mut Frame<'_>, area: Rect, states: [State; 3]) {
let horizontal = Layout::horizontal([
Constraint::Length(15),
Constraint::Length(15),
Constraint::Length(15),
Constraint::Min(0), // ignore remaining space
]);
let [red, green, blue, _] = horizontal.areas(area);
frame.render_widget(Button::new("Red").theme(RED).state(states[0]), red);
frame.render_widget(Button::new("Green").theme(GREEN).state(states[1]), green);
frame.render_widget(Button::new("Blue").theme(BLUE).state(states[2]), blue);
}
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,12 +1,16 @@
use crate::util::{RandomSignal, SinSignal, StatefulList, TabsState};
use rand::{
distributions::{Distribution, Uniform},
rngs::ThreadRng,
};
use ratatui::widgets::ListState;
const TASKS: [&'static str; 24] = [
const TASKS: [&str; 24] = [
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10",
"Item11", "Item12", "Item13", "Item14", "Item15", "Item16", "Item17", "Item18", "Item19",
"Item20", "Item21", "Item22", "Item23", "Item24",
];
const LOGS: [(&'static str, &'static str); 26] = [
const LOGS: [(&str, &str); 26] = [
("Event1", "INFO"),
("Event2", "INFO"),
("Event3", "CRITICAL"),
@@ -35,7 +39,7 @@ const LOGS: [(&'static str, &'static str); 26] = [
("Event26", "INFO"),
];
const EVENTS: [(&'static str, u64); 24] = [
const EVENTS: [(&str, u64); 24] = [
("B1", 9),
("B2", 12),
("B3", 5),
@@ -62,6 +66,120 @@ const EVENTS: [(&'static str, u64); 24] = [
("B24", 5),
];
#[derive(Clone)]
pub struct RandomSignal {
distribution: Uniform<u64>,
rng: ThreadRng,
}
impl RandomSignal {
pub fn new(lower: u64, upper: u64) -> Self {
Self {
distribution: Uniform::new(lower, upper),
rng: rand::thread_rng(),
}
}
}
impl Iterator for RandomSignal {
type Item = u64;
fn next(&mut self) -> Option<u64> {
Some(self.distribution.sample(&mut self.rng))
}
}
#[derive(Clone)]
pub struct SinSignal {
x: f64,
interval: f64,
period: f64,
scale: f64,
}
impl SinSignal {
pub const fn new(interval: f64, period: f64, scale: f64) -> Self {
Self {
x: 0.0,
interval,
period,
scale,
}
}
}
impl Iterator for SinSignal {
type Item = (f64, f64);
fn next(&mut self) -> Option<Self::Item> {
let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
self.x += self.interval;
Some(point)
}
}
pub struct TabsState<'a> {
pub titles: Vec<&'a str>,
pub index: usize,
}
impl<'a> TabsState<'a> {
pub const fn new(titles: Vec<&'a str>) -> Self {
Self { titles, index: 0 }
}
pub fn next(&mut self) {
self.index = (self.index + 1) % self.titles.len();
}
pub fn previous(&mut self) {
if self.index > 0 {
self.index -= 1;
} else {
self.index = self.titles.len() - 1;
}
}
}
pub struct StatefulList<T> {
pub state: ListState,
pub items: Vec<T>,
}
impl<T> StatefulList<T> {
pub fn with_items(items: Vec<T>) -> Self {
Self {
state: ListState::default(),
items,
}
}
pub fn next(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i >= self.items.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.state.select(Some(i));
}
pub fn previous(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
self.items.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.state.select(Some(i));
}
}
pub struct Signal<S: Iterator> {
source: S,
pub points: Vec<S::Item>,
@@ -73,9 +191,7 @@ where
S: Iterator,
{
fn on_tick(&mut self) {
for _ in 0..self.tick_rate {
self.points.remove(0);
}
self.points.drain(0..self.tick_rate);
self.points
.extend(self.source.by_ref().take(self.tick_rate));
}
@@ -119,7 +235,7 @@ pub struct App<'a> {
}
impl<'a> App<'a> {
pub fn new(title: &'a str, enhanced_graphics: bool) -> App<'a> {
pub fn new(title: &'a str, enhanced_graphics: bool) -> Self {
let mut rand_signal = RandomSignal::new(0, 100);
let sparkline_points = rand_signal.by_ref().take(300).collect();
let mut sin_signal = SinSignal::new(0.2, 3.0, 18.0);
@@ -129,7 +245,7 @@ impl<'a> App<'a> {
App {
title,
should_quit: false,
tabs: TabsState::new(vec!["Tab0", "Tab1"]),
tabs: TabsState::new(vec!["Tab0", "Tab1", "Tab2"]),
show_chart: true,
progress: 0.0,
sparkline: Signal {

View File

@@ -0,0 +1,79 @@
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
Terminal,
};
use crate::{app::App, ui};
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let app = App::new("Crossterm Demo", enhanced_graphics);
let app_result = 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) = app_result {
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(|frame| ui::draw(frame, &mut app))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if 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),
_ => {}
}
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
if app.should_quit {
return Ok(());
}
}
}

55
examples/demo/main.rs Normal file
View File

@@ -0,0 +1,55 @@
//! # [Ratatui] Original Demo example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use std::{error::Error, time::Duration};
use argh::FromArgs;
mod app;
#[cfg(feature = "crossterm")]
mod crossterm;
#[cfg(all(not(windows), feature = "termion"))]
mod termion;
#[cfg(feature = "termwiz")]
mod termwiz;
mod ui;
/// Demo
#[derive(Debug, FromArgs)]
struct Cli {
/// time in ms between two ticks.
#[argh(option, default = "250")]
tick_rate: u64,
/// whether unicode symbols are used to improve the overall look of the app
#[argh(option, default = "true")]
enhanced_graphics: bool,
}
fn main() -> Result<(), Box<dyn Error>> {
let cli: Cli = argh::from_env();
let tick_rate = Duration::from_millis(cli.tick_rate);
#[cfg(feature = "crossterm")]
crate::crossterm::run(tick_rate, cli.enhanced_graphics)?;
#[cfg(all(not(windows), feature = "termion", not(feature = "crossterm")))]
crate::termion::run(tick_rate, cli.enhanced_graphics)?;
#[cfg(all(
feature = "termwiz",
not(feature = "crossterm"),
not(feature = "termion")
))]
crate::termwiz::run(tick_rate, cli.enhanced_graphics)?;
Ok(())
}

View File

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

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

@@ -0,0 +1,86 @@
#![allow(dead_code)]
use std::{error::Error, io, sync::mpsc, thread, time::Duration};
use ratatui::{
backend::{Backend, TermionBackend},
termion::{
event::Key,
input::{MouseTerminal, TermRead},
raw::IntoRawMode,
screen::IntoAlternateScreen,
},
Terminal,
};
use crate::{app::App, ui};
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
// setup terminal
let stdout = io::stdout()
.into_raw_mode()
.unwrap()
.into_alternate_screen()
.unwrap();
let stdout = MouseTerminal::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let app = App::new("Termion demo", enhanced_graphics);
run_app(&mut terminal, app, tick_rate)?;
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> Result<(), Box<dyn Error>> {
let events = events(tick_rate);
loop {
terminal.draw(|frame| ui::draw(frame, &mut app))?;
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),
_ => {}
},
Event::Tick => app.on_tick(),
}
if app.should_quit {
return Ok(());
}
}
}
enum Event {
Input(Key),
Tick,
}
fn events(tick_rate: Duration) -> mpsc::Receiver<Event> {
let (tx, rx) = mpsc::channel();
let keys_tx = tx.clone();
thread::spawn(move || {
let stdin = io::stdin();
for key in stdin.keys().flatten() {
if let Err(err) = keys_tx.send(Event::Input(key)) {
eprintln!("{err}");
return;
}
}
});
thread::spawn(move || loop {
if let Err(err) = tx.send(Event::Tick) {
eprintln!("{err}");
break;
}
thread::sleep(tick_rate);
});
rx
}

80
examples/demo/termwiz.rs Normal file
View File

@@ -0,0 +1,80 @@
#![allow(dead_code)]
use std::{
error::Error,
time::{Duration, Instant},
};
use ratatui::{
backend::TermwizBackend,
termwiz::{
input::{InputEvent, KeyCode},
terminal::Terminal as TermwizTerminal,
},
Terminal,
};
use crate::{app::App, ui};
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
let backend = TermwizBackend::new()?;
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
// create app and run it
let app = App::new("Termwiz Demo", enhanced_graphics);
let app_result = run_app(&mut terminal, app, tick_rate);
terminal.show_cursor()?;
terminal.flush()?;
if let Err(err) = app_result {
println!("{err:?}");
}
Ok(())
}
fn run_app(
terminal: &mut Terminal<TermwizBackend>,
mut app: App,
tick_rate: Duration,
) -> Result<(), Box<dyn Error>> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|frame| ui::draw(frame, &mut app))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if let Some(input) = terminal
.backend_mut()
.buffered_terminal_mut()
.terminal()
.poll_input(Some(timeout))?
{
match input {
InputEvent::Key(key_code) => match key_code.key {
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),
_ => {}
},
InputEvent::Resized { cols, rows } => {
terminal
.backend_mut()
.buffered_terminal_mut()
.resize(cols, rows);
}
_ => {}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
if app.should_quit {
return Ok(());
}
}
}

View File

@@ -1,81 +1,77 @@
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
use ratatui::{
layout::{Constraint, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle},
text::{self, Span},
widgets::{
Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, List, Paragraph, Row, Sparkline,
Table, Tabs, Text,
canvas::{self, Canvas, Circle, Map, MapResolution, Rectangle},
Axis, BarChart, Block, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem, Paragraph,
Row, Sparkline, Table, Tabs, Wrap,
},
Frame,
};
use crate::demo::App;
use crate::app::App;
pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let chunks = Layout::default()
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
.split(f.size());
let tabs = Tabs::default()
.block(Block::default().borders(Borders::ALL).title(app.title))
.titles(&app.tabs.titles)
.style(Style::default().fg(Color::Green))
pub fn draw(frame: &mut Frame, app: &mut App) {
let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(frame.area());
let tabs = app
.tabs
.titles
.iter()
.map(|t| text::Line::from(Span::styled(*t, Style::default().fg(Color::Green))))
.collect::<Tabs>()
.block(Block::bordered().title(app.title))
.highlight_style(Style::default().fg(Color::Yellow))
.select(app.tabs.index);
f.render_widget(tabs, chunks[0]);
frame.render_widget(tabs, chunks[0]);
match app.tabs.index {
0 => draw_first_tab(f, app, chunks[1]),
1 => draw_second_tab(f, app, chunks[1]),
0 => draw_first_tab(frame, app, chunks[1]),
1 => draw_second_tab(frame, app, chunks[1]),
2 => draw_third_tab(frame, app, chunks[1]),
_ => {}
};
}
fn draw_first_tab<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
where
B: Backend,
{
let chunks = Layout::default()
.constraints(
[
Constraint::Length(7),
Constraint::Min(7),
Constraint::Length(7),
]
.as_ref(),
)
.split(area);
draw_gauges(f, app, chunks[0]);
draw_charts(f, app, chunks[1]);
draw_text(f, chunks[2]);
fn draw_first_tab(frame: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::vertical([
Constraint::Length(9),
Constraint::Min(8),
Constraint::Length(7),
])
.split(area);
draw_gauges(frame, app, chunks[0]);
draw_charts(frame, app, chunks[1]);
draw_text(frame, chunks[2]);
}
fn draw_gauges<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
where
B: Backend,
{
let chunks = Layout::default()
.constraints([Constraint::Length(2), Constraint::Length(3)].as_ref())
.margin(1)
.split(area);
let block = Block::default().borders(Borders::ALL).title("Graphs");
f.render_widget(block, area);
fn draw_gauges(frame: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::vertical([
Constraint::Length(2),
Constraint::Length(3),
Constraint::Length(2),
])
.margin(1)
.split(area);
let block = Block::bordered().title("Graphs");
frame.render_widget(block, area);
let label = format!("{:.2}%", app.progress * 100.0);
let gauge = Gauge::default()
.block(Block::default().title("Gauge:"))
.style(
.block(Block::new().title("Gauge:"))
.gauge_style(
Style::default()
.fg(Color::Magenta)
.bg(Color::Black)
.modifier(Modifier::ITALIC | Modifier::BOLD),
.add_modifier(Modifier::ITALIC | Modifier::BOLD),
)
.label(&label)
.use_unicode(app.enhanced_graphics)
.label(label)
.ratio(app.progress);
f.render_widget(gauge, chunks[0]);
frame.render_widget(gauge, chunks[0]);
let sparkline = Sparkline::default()
.block(Block::default().title("Sparkline:"))
.block(Block::new().title("Sparkline:"))
.style(Style::default().fg(Color::Green))
.data(&app.sparkline.points)
.bar_set(if app.enhanced_graphics {
@@ -83,62 +79,78 @@ where
} else {
symbols::bar::THREE_LEVELS
});
f.render_widget(sparkline, chunks[1]);
frame.render_widget(sparkline, chunks[1]);
let line_gauge = LineGauge::default()
.block(Block::new().title("LineGauge:"))
.filled_style(Style::default().fg(Color::Magenta))
.line_set(if app.enhanced_graphics {
symbols::line::THICK
} else {
symbols::line::NORMAL
})
.ratio(app.progress);
frame.render_widget(line_gauge, chunks[2]);
}
fn draw_charts<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
where
B: Backend,
{
#[allow(clippy::too_many_lines)]
fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) {
let constraints = if app.show_chart {
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
} else {
vec![Constraint::Percentage(100)]
};
let chunks = Layout::default()
.constraints(constraints)
.direction(Direction::Horizontal)
.split(area);
let chunks = Layout::horizontal(constraints).split(area);
{
let chunks = Layout::default()
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
let chunks = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[0]);
{
let chunks = Layout::default()
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.direction(Direction::Horizontal)
.split(chunks[0]);
let chunks =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[0]);
// Draw tasks
let tasks = app.tasks.items.iter().map(|i| Text::raw(*i));
let tasks: Vec<ListItem> = app
.tasks
.items
.iter()
.map(|i| ListItem::new(vec![text::Line::from(Span::raw(*i))]))
.collect();
let tasks = List::new(tasks)
.block(Block::default().borders(Borders::ALL).title("List"))
.highlight_style(Style::default().fg(Color::Yellow).modifier(Modifier::BOLD))
.block(Block::bordered().title("List"))
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
.highlight_symbol("> ");
f.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state);
frame.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state);
// Draw logs
let info_style = Style::default().fg(Color::White);
let info_style = Style::default().fg(Color::Blue);
let warning_style = Style::default().fg(Color::Yellow);
let error_style = Style::default().fg(Color::Magenta);
let critical_style = Style::default().fg(Color::Red);
let logs = app.logs.items.iter().map(|&(evt, level)| {
Text::styled(
format!("{}: {}", level, evt),
match level {
let logs: Vec<ListItem> = app
.logs
.items
.iter()
.map(|&(evt, level)| {
let s = match level {
"ERROR" => error_style,
"CRITICAL" => critical_style,
"WARNING" => warning_style,
_ => info_style,
},
)
});
let logs = List::new(logs).block(Block::default().borders(Borders::ALL).title("List"));
f.render_stateful_widget(logs, chunks[1], &mut app.logs.state);
};
let content = vec![text::Line::from(vec![
Span::styled(format!("{level:<9}"), s),
Span::raw(evt),
])];
ListItem::new(content)
})
.collect();
let logs = List::new(logs).block(Block::bordered().title("List"));
frame.render_stateful_widget(logs, chunks[1], &mut app.logs.state);
}
let barchart = BarChart::default()
.block(Block::default().borders(Borders::ALL).title("Bar chart"))
.block(Block::bordered().title("Bar chart"))
.data(&app.barchart)
.bar_width(3)
.bar_gap(2)
@@ -151,19 +163,28 @@ where
Style::default()
.fg(Color::Black)
.bg(Color::Green)
.modifier(Modifier::ITALIC),
.add_modifier(Modifier::ITALIC),
)
.label_style(Style::default().fg(Color::Yellow))
.style(Style::default().fg(Color::Green));
f.render_widget(barchart, chunks[1]);
.bar_style(Style::default().fg(Color::Green));
frame.render_widget(barchart, chunks[1]);
}
if app.show_chart {
let x_labels = [
format!("{}", app.signals.window[0]),
format!("{}", (app.signals.window[0] + app.signals.window[1]) / 2.0),
format!("{}", app.signals.window[1]),
let x_labels = vec![
Span::styled(
format!("{}", app.signals.window[0]),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(format!(
"{}",
(app.signals.window[0] + app.signals.window[1]) / 2.0
)),
Span::styled(
format!("{}", app.signals.window[1]),
Style::default().add_modifier(Modifier::BOLD),
),
];
let datasets = [
let datasets = vec![
Dataset::default()
.name("data2")
.marker(symbols::Marker::Dot)
@@ -179,96 +200,108 @@ where
.style(Style::default().fg(Color::Yellow))
.data(&app.signals.sin2.points),
];
let chart = Chart::default()
let chart = Chart::new(datasets)
.block(
Block::default()
.title("Chart")
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD))
.borders(Borders::ALL),
Block::bordered().title(Span::styled(
"Chart",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds(app.signals.window)
.labels(&x_labels),
.labels(x_labels),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([-20.0, 20.0])
.labels(&["-20", "0", "20"]),
)
.datasets(&datasets);
f.render_widget(chart, chunks[1]);
.labels([
Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("0"),
Span::styled("20", Style::default().add_modifier(Modifier::BOLD)),
]),
);
frame.render_widget(chart, chunks[1]);
}
}
fn draw_text<B>(f: &mut Frame<B>, area: Rect)
where
B: Backend,
{
let text = [
Text::raw("This is a paragraph with several lines. You can change style your text the way you want.\n\nFox example: "),
Text::styled("under", Style::default().fg(Color::Red)),
Text::raw(" "),
Text::styled("the", Style::default().fg(Color::Green)),
Text::raw(" "),
Text::styled("rainbow", Style::default().fg(Color::Blue)),
Text::raw(".\nOh and if you didn't "),
Text::styled("notice", Style::default().modifier(Modifier::ITALIC)),
Text::raw(" you can "),
Text::styled("automatically", Style::default().modifier(Modifier::BOLD)),
Text::raw(" "),
Text::styled("wrap", Style::default().modifier(Modifier::REVERSED)),
Text::raw(" your "),
Text::styled("text", Style::default().modifier(Modifier::UNDERLINED)),
Text::raw(".\nOne more thing is that it should display unicode characters: 10€")
fn draw_text(frame: &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(""),
text::Line::from(vec![
Span::from("For example: "),
Span::styled("under", Style::default().fg(Color::Red)),
Span::raw(" "),
Span::styled("the", Style::default().fg(Color::Green)),
Span::raw(" "),
Span::styled("rainbow", Style::default().fg(Color::Blue)),
Span::raw("."),
]),
text::Line::from(vec![
Span::raw("Oh and if you didn't "),
Span::styled("notice", Style::default().add_modifier(Modifier::ITALIC)),
Span::raw(" you can "),
Span::styled("automatically", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" "),
Span::styled("wrap", Style::default().add_modifier(Modifier::REVERSED)),
Span::raw(" your "),
Span::styled("text", Style::default().add_modifier(Modifier::UNDERLINED)),
Span::raw(".")
]),
text::Line::from(
"One more thing is that it should display unicode characters: 10€"
),
];
let block = Block::default()
.borders(Borders::ALL)
.title("Footer")
.title_style(Style::default().fg(Color::Magenta).modifier(Modifier::BOLD));
let paragraph = Paragraph::new(text.iter()).block(block).wrap(true);
f.render_widget(paragraph, area);
let block = Block::bordered().title(Span::styled(
"Footer",
Style::default()
.fg(Color::Magenta)
.add_modifier(Modifier::BOLD),
));
let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
frame.render_widget(paragraph, area);
}
fn draw_second_tab<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
where
B: Backend,
{
let chunks = Layout::default()
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)].as_ref())
.direction(Direction::Horizontal)
.split(area);
fn draw_second_tab(frame: &mut Frame, app: &mut App, area: Rect) {
let chunks =
Layout::horizontal([Constraint::Percentage(30), Constraint::Percentage(70)]).split(area);
let up_style = Style::default().fg(Color::Green);
let failure_style = Style::default()
.fg(Color::Red)
.modifier(Modifier::RAPID_BLINK | Modifier::CROSSED_OUT);
let header = ["Server", "Location", "Status"];
.add_modifier(Modifier::RAPID_BLINK | Modifier::CROSSED_OUT);
let rows = app.servers.iter().map(|s| {
let style = if s.status == "Up" {
up_style
} else {
failure_style
};
Row::StyledData(vec![s.name, s.location, s.status].into_iter(), style)
Row::new(vec![s.name, s.location, s.status]).style(style)
});
let table = Table::new(header.iter(), rows)
.block(Block::default().title("Servers").borders(Borders::ALL))
.header_style(Style::default().fg(Color::Yellow))
.widths(&[
let table = Table::new(
rows,
[
Constraint::Length(15),
Constraint::Length(15),
Constraint::Length(10),
]);
f.render_widget(table, chunks[0]);
],
)
.header(
Row::new(vec!["Server", "Location", "Status"])
.style(Style::default().fg(Color::Yellow))
.bottom_margin(1),
)
.block(Block::bordered().title("Servers"));
frame.render_widget(table, chunks[0]);
let map = Canvas::default()
.block(Block::default().title("World").borders(Borders::ALL))
.block(Block::bordered().title("World"))
.paint(|ctx| {
ctx.draw(&Map {
color: Color::White,
@@ -282,9 +315,15 @@ where
height: 10.0,
color: Color::Yellow,
});
ctx.draw(&Circle {
x: app.servers[2].coords.1,
y: app.servers[2].coords.0,
radius: 10.0,
color: Color::Green,
});
for (i, s1) in app.servers.iter().enumerate() {
for s2 in &app.servers[i + 1..] {
ctx.draw(&Line {
ctx.draw(&canvas::Line {
x1: s1.coords.1,
y1: s1.coords.0,
y2: s2.coords.0,
@@ -299,7 +338,11 @@ where
} else {
Color::Red
};
ctx.print(server.coords.1, server.coords.0, "X", color);
ctx.print(
server.coords.1,
server.coords.0,
Span::styled("X", Style::default().fg(color)),
);
}
})
.marker(if app.enhanced_graphics {
@@ -309,5 +352,49 @@ where
})
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0]);
f.render_widget(map, chunks[1]);
frame.render_widget(map, chunks[1]);
}
fn draw_third_tab(frame: &mut Frame, _app: &mut App, area: Rect) {
let chunks = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).split(area);
let colors = [
Color::Reset,
Color::Black,
Color::Red,
Color::Green,
Color::Yellow,
Color::Blue,
Color::Magenta,
Color::Cyan,
Color::Gray,
Color::DarkGray,
Color::LightRed,
Color::LightGreen,
Color::LightYellow,
Color::LightBlue,
Color::LightMagenta,
Color::LightCyan,
Color::White,
];
let items: Vec<Row> = colors
.iter()
.map(|c| {
let cells = vec![
Cell::from(Span::raw(format!("{c:?}: "))),
Cell::from(Span::styled("Foreground", Style::default().fg(*c))),
Cell::from(Span::styled("Background", Style::default().bg(*c))),
];
Row::new(cells)
})
.collect();
let table = Table::new(
items,
[
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
],
)
.block(Block::bordered().title("Colors"));
frame.render_widget(table, chunks[0]);
}

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

@@ -0,0 +1,225 @@
use std::time::Duration;
use color_eyre::{eyre::Context, Result};
use crossterm::event;
use itertools::Itertools;
use ratatui::{
buffer::Buffer,
crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind},
layout::{Constraint, Layout, Rect},
style::Color,
text::{Line, Span},
widgets::{Block, Tabs, Widget},
DefaultTerminal, Frame,
};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
use crate::{
destroy,
tabs::{AboutTab, EmailTab, RecipeTab, TracerouteTab, WeatherTab},
THEME,
};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct App {
mode: Mode,
tab: Tab,
about_tab: AboutTab,
recipe_tab: RecipeTab,
email_tab: EmailTab,
traceroute_tab: TracerouteTab,
weather_tab: WeatherTab,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum Mode {
#[default]
Running,
Destroy,
Quit,
}
#[derive(Debug, Clone, Copy, Default, Display, EnumIter, FromRepr, PartialEq, Eq)]
enum Tab {
#[default]
About,
Recipe,
Email,
Traceroute,
Weather,
}
impl App {
/// Run the app until the user quits.
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
while self.is_running() {
terminal
.draw(|frame| self.draw(frame))
.wrap_err("terminal.draw")?;
self.handle_events()?;
}
Ok(())
}
fn is_running(&self) -> bool {
self.mode != Mode::Quit
}
/// Draw a single frame of the app.
fn draw(&self, frame: &mut Frame) {
frame.render_widget(self, frame.area());
if self.mode == Mode::Destroy {
destroy::destroy(frame);
}
}
/// Handle events from the terminal.
///
/// This function is called once per frame, The events are polled from the stdin with timeout of
/// 1/50th of a second. This was chosen to try to match the default frame rate of a GIF in VHS.
fn handle_events(&mut self) -> Result<()> {
let timeout = Duration::from_secs_f64(1.0 / 50.0);
if !event::poll(timeout)? {
return Ok(());
}
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => self.handle_key_press(key),
_ => {}
}
Ok(())
}
fn handle_key_press(&mut self, key: KeyEvent) {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => self.mode = Mode::Quit,
KeyCode::Char('h') | KeyCode::Left => self.prev_tab(),
KeyCode::Char('l') | KeyCode::Right => self.next_tab(),
KeyCode::Char('k') | KeyCode::Up => self.prev(),
KeyCode::Char('j') | KeyCode::Down => self.next(),
KeyCode::Char('d') | KeyCode::Delete => self.destroy(),
_ => {}
};
}
fn prev(&mut self) {
match self.tab {
Tab::About => self.about_tab.prev_row(),
Tab::Recipe => self.recipe_tab.prev(),
Tab::Email => self.email_tab.prev(),
Tab::Traceroute => self.traceroute_tab.prev_row(),
Tab::Weather => self.weather_tab.prev(),
}
}
fn next(&mut self) {
match self.tab {
Tab::About => self.about_tab.next_row(),
Tab::Recipe => self.recipe_tab.next(),
Tab::Email => self.email_tab.next(),
Tab::Traceroute => self.traceroute_tab.next_row(),
Tab::Weather => self.weather_tab.next(),
}
}
fn prev_tab(&mut self) {
self.tab = self.tab.prev();
}
fn next_tab(&mut self) {
self.tab = self.tab.next();
}
fn destroy(&mut self) {
self.mode = Mode::Destroy;
}
}
/// Implement Widget for &App rather than for App as we would otherwise have to clone or copy the
/// entire app state on every frame. For this example, the app state is small enough that it doesn't
/// matter, but for larger apps this can be a significant performance improvement.
impl Widget for &App {
fn render(self, area: Rect, buf: &mut Buffer) {
let vertical = Layout::vertical([
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
]);
let [title_bar, tab, bottom_bar] = vertical.areas(area);
Block::new().style(THEME.root).render(area, buf);
self.render_title_bar(title_bar, buf);
self.render_selected_tab(tab, buf);
App::render_bottom_bar(bottom_bar, buf);
}
}
impl App {
fn render_title_bar(&self, area: Rect, buf: &mut Buffer) {
let layout = Layout::horizontal([Constraint::Min(0), Constraint::Length(43)]);
let [title, tabs] = layout.areas(area);
Span::styled("Ratatui", THEME.app_title).render(title, buf);
let titles = Tab::iter().map(Tab::title);
Tabs::new(titles)
.style(THEME.tabs)
.highlight_style(THEME.tabs_selected)
.select(self.tab as usize)
.divider("")
.padding("", "")
.render(tabs, buf);
}
fn render_selected_tab(&self, area: Rect, buf: &mut Buffer) {
match self.tab {
Tab::About => self.about_tab.render(area, buf),
Tab::Recipe => self.recipe_tab.render(area, buf),
Tab::Email => self.email_tab.render(area, buf),
Tab::Traceroute => self.traceroute_tab.render(area, buf),
Tab::Weather => self.weather_tab.render(area, buf),
};
}
fn render_bottom_bar(area: Rect, buf: &mut Buffer) {
let keys = [
("H/←", "Left"),
("L/→", "Right"),
("K/↑", "Up"),
("J/↓", "Down"),
("D/Del", "Destroy"),
("Q/Esc", "Quit"),
];
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();
Line::from(spans)
.centered()
.style((Color::Indexed(236), Color::Indexed(232)))
.render(area, buf);
}
}
impl Tab {
fn next(self) -> Self {
let current_index = self as usize;
let next_index = current_index.saturating_add(1);
Self::from_repr(next_index).unwrap_or(self)
}
fn prev(self) -> Self {
let current_index = self as usize;
let prev_index = current_index.saturating_sub(1);
Self::from_repr(prev_index).unwrap_or(self)
}
fn title(self) -> String {
match self {
Self::About => String::new(),
tab => format!(" {tab} "),
}
}
}

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

@@ -0,0 +1,35 @@
use palette::{IntoColor, Okhsv, Srgb};
use ratatui::{buffer::Buffer, layout::Rect, style::Color, widgets::Widget};
/// 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 {
#[allow(clippy::cast_precision_loss, clippy::similar_names)]
fn render(self, area: Rect, buf: &mut Buffer) {
for (yi, y) in (area.top()..area.bottom()).enumerate() {
let value = f32::from(area.height) - yi as f32;
let value_fg = value / f32::from(area.height);
let value_bg = (value - 0.5) / f32::from(area.height);
for (xi, x) in (area.left()..area.right()).enumerate() {
let hue = xi as f32 * 360.0 / f32::from(area.width);
let fg = color_from_oklab(hue, Okhsv::max_saturation(), value_fg);
let bg = color_from_oklab(hue, Okhsv::max_saturation(), value_bg);
buf[(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)
}

142
examples/demo2/destroy.rs Normal file
View File

@@ -0,0 +1,142 @@
use rand::Rng;
use rand_chacha::rand_core::SeedableRng;
use ratatui::{
buffer::Buffer,
layout::{Flex, Layout, Rect},
style::{Color, Style},
text::Text,
widgets::Widget,
Frame,
};
/// delay the start of the animation so it doesn't start immediately
const DELAY: usize = 120;
/// higher means more pixels per frame are modified in the animation
const DRIP_SPEED: usize = 500;
/// delay the start of the text animation so it doesn't start immediately after the initial delay
const TEXT_DELAY: usize = 180;
/// Destroy mode activated by pressing `d`
pub fn destroy(frame: &mut Frame<'_>) {
let frame_count = frame.count().saturating_sub(DELAY);
if frame_count == 0 {
return;
}
let area = frame.area();
let buf = frame.buffer_mut();
drip(frame_count, area, buf);
text(frame_count, area, buf);
}
/// Move a bunch of random pixels down one row.
///
/// Each pick some random pixels and move them each down one row. This is a very inefficient way to
/// do this, but it works well enough for this demo.
#[allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss
)]
fn drip(frame_count: usize, area: Rect, buf: &mut Buffer) {
// a seeded rng as we have to move the same random pixels each frame
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(10);
let ramp_frames = 450;
let fractional_speed = frame_count as f64 / f64::from(ramp_frames);
let variable_speed = DRIP_SPEED as f64 * fractional_speed * fractional_speed * fractional_speed;
let pixel_count = (frame_count as f64 * variable_speed).floor() as usize;
for _ in 0..pixel_count {
let src_x = rng.gen_range(0..area.width);
let src_y = rng.gen_range(1..area.height - 2);
let src = buf[(src_x, src_y)].clone();
// 1% of the time, move a blank or pixel (10:1) to the top line of the screen
if rng.gen_ratio(1, 100) {
let dest_x = rng
.gen_range(src_x.saturating_sub(5)..src_x.saturating_add(5))
.clamp(area.left(), area.right() - 1);
let dest_y = area.top() + 1;
let dest = &mut buf[(dest_x, dest_y)];
// copy the cell to the new location about 1/10 of the time blank out the cell the rest
// of the time. This has the effect of gradually removing the pixels from the screen.
if rng.gen_ratio(1, 10) {
*dest = src;
} else {
dest.reset();
}
} else {
// move the pixel down one row
let dest_x = src_x;
let dest_y = src_y.saturating_add(1).min(area.bottom() - 2);
// copy the cell to the new location
buf[(dest_x, dest_y)] = src;
}
}
}
/// draw some text fading in and out from black to red and back
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
fn text(frame_count: usize, area: Rect, buf: &mut Buffer) {
let sub_frame = frame_count.saturating_sub(TEXT_DELAY);
if sub_frame == 0 {
return;
}
let logo = indoc::indoc! {"
██████ ████ ██████ ████ ██████ ██ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ████████ ██ ████████ ██ ██ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ████ ██
"};
let logo_text = Text::styled(logo, Color::Rgb(255, 255, 255));
let area = centered_rect(area, logo_text.width() as u16, logo_text.height() as u16);
let mask_buf = &mut Buffer::empty(area);
logo_text.render(area, mask_buf);
let percentage = (sub_frame as f64 / 480.0).clamp(0.0, 1.0);
for row in area.rows() {
for col in row.columns() {
let cell = &mut buf[(col.x, col.y)];
let mask_cell = &mut mask_buf[(col.x, col.y)];
cell.set_symbol(mask_cell.symbol());
// blend the mask cell color with the cell color
let cell_color = cell.style().bg.unwrap_or(Color::Rgb(0, 0, 0));
let mask_color = mask_cell.style().fg.unwrap_or(Color::Rgb(255, 0, 0));
let color = blend(mask_color, cell_color, percentage);
cell.set_style(Style::new().fg(color));
}
}
}
fn blend(mask_color: Color, cell_color: Color, percentage: f64) -> Color {
let Color::Rgb(mask_red, mask_green, mask_blue) = mask_color else {
return mask_color;
};
let Color::Rgb(cell_red, cell_green, cell_blue) = cell_color else {
return mask_color;
};
let remain = 1.0 - percentage;
let red = f64::from(mask_red).mul_add(percentage, f64::from(cell_red) * remain);
let green = f64::from(mask_green).mul_add(percentage, f64::from(cell_green) * remain);
let blue = f64::from(mask_blue).mul_add(percentage, f64::from(cell_blue) * remain);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
Color::Rgb(red as u8, green as u8, blue as u8)
}
/// a centered rect of the given size
fn centered_rect(area: Rect, width: u16, height: u16) -> Rect {
let horizontal = Layout::horizontal([width]).flex(Flex::Center);
let vertical = Layout::vertical([height]).flex(Flex::Center);
let [area] = vertical.areas(area);
let [area] = horizontal.areas(area);
area
}

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

@@ -0,0 +1,54 @@
//! # [Ratatui] Demo2 example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
#![allow(
clippy::missing_errors_doc,
clippy::module_name_repetitions,
clippy::must_use_candidate
)]
mod app;
mod colors;
mod destroy;
mod tabs;
mod theme;
use std::io::stdout;
use app::App;
use color_eyre::Result;
use crossterm::{
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{layout::Rect, TerminalOptions, Viewport};
pub use self::{
colors::{color_from_oklab, RgbSwatch},
theme::THEME,
};
fn main() -> Result<()> {
color_eyre::install()?;
// 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 viewport = Viewport::Fixed(Rect::new(0, 0, 81, 18));
let terminal = ratatui::init_with_options(TerminalOptions { viewport });
execute!(stdout(), EnterAlternateScreen).expect("failed to enter alternate screen");
let app_result = App::default().run(terminal);
execute!(stdout(), LeaveAlternateScreen).expect("failed to leave alternate screen");
ratatui::restore();
app_result
}

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,165 @@
use itertools::Itertools;
use ratatui::{
buffer::Buffer,
layout::{Alignment, Constraint, Layout, Margin, Rect},
widgets::{Block, Borders, Clear, Padding, Paragraph, Widget, Wrap},
};
use crate::{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█ █ ",
];
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct AboutTab {
row_index: usize,
}
impl AboutTab {
pub fn prev_row(&mut self) {
self.row_index = self.row_index.saturating_sub(1);
}
pub fn next_row(&mut self) {
self.row_index = self.row_index.saturating_add(1);
}
}
impl Widget for AboutTab {
fn render(self, area: Rect, buf: &mut Buffer) {
RgbSwatch.render(area, buf);
let horizontal = Layout::horizontal([Constraint::Length(34), Constraint::Min(0)]);
let [description, logo] = horizontal.areas(area);
render_crate_description(description, buf);
render_logo(self.row_index, logo, 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.
#[allow(clippy::cast_possible_truncation)]
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 = &mut buf[(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.bg = 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,158 @@
use itertools::Itertools;
use ratatui::{
buffer::Buffer,
layout::{Constraint, Layout, Margin, Rect},
style::{Styled, Stylize},
text::Line,
widgets::{
Block, BorderType, Borders, Clear, List, ListItem, ListState, Padding, Paragraph,
Scrollbar, ScrollbarState, StatefulWidget, Tabs, Widget,
},
};
use unicode_width::UnicodeWidthStr;
use crate::{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, Clone, Copy, Default, PartialEq, Eq)]
pub struct EmailTab {
row_index: usize,
}
impl EmailTab {
/// Select the previous email (with wrap around).
pub fn prev(&mut self) {
self.row_index = self.row_index.saturating_add(EMAILS.len() - 1) % EMAILS.len();
}
/// Select the next email (with wrap around).
pub fn next(&mut self) {
self.row_index = self.row_index.saturating_add(1) % 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 vertical = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]);
let [inbox, email] = vertical.areas(area);
render_inbox(self.row_index, inbox, buf);
render_email(self.row_index, email, buf);
}
}
fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
let [tabs, inbox] = vertical.areas(area);
let theme = THEME.email;
Tabs::new(vec![" Inbox ", " Sent ", " Drafts "])
.style(theme.tabs)
.highlight_style(theme.tabs_selected)
.select(0)
.divider("")
.render(tabs, 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()]))
});
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),
inbox,
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(inbox, 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 vertical = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]);
let [headers_area, body_area] = vertical.areas(inner);
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(headers_area, buf);
let body = email.body.lines().map(Line::from).collect_vec();
Paragraph::new(body)
.style(theme.body)
.render(body_area, buf);
} else {
Paragraph::new("No email selected").render(inner, buf);
}
}

View File

@@ -0,0 +1,185 @@
use itertools::Itertools;
use ratatui::{
buffer::Buffer,
layout::{Alignment, Constraint, Layout, Margin, Rect},
style::{Style, Stylize},
text::Line,
widgets::{
Block, Clear, Padding, Paragraph, Row, Scrollbar, ScrollbarOrientation, ScrollbarState,
StatefulWidget, Table, TableState, Widget, Wrap,
},
};
use crate::{RgbSwatch, THEME};
#[derive(Debug, Default, Clone, Copy)]
struct Ingredient {
quantity: &'static str,
name: &'static str,
}
impl Ingredient {
#[allow(clippy::cast_possible_truncation)]
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, Default, Clone, Copy, PartialEq, Eq)]
pub struct RecipeTab {
row_index: usize,
}
impl RecipeTab {
/// Select the previous item in the ingredients list (with wrap around)
pub fn prev(&mut self) {
self.row_index = self.row_index.saturating_add(INGREDIENTS.len() - 1) % INGREDIENTS.len();
}
/// Select the next item in the ingredients list (with wrap around)
pub fn next(&mut self) {
self.row_index = self.row_index.saturating_add(1) % 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.row_index, scrollbar_area, buf);
let area = area.inner(Margin {
horizontal: 2,
vertical: 1,
});
let [recipe, ingredients] =
Layout::horizontal([Constraint::Length(44), Constraint::Min(0)]).areas(area);
render_recipe(recipe, buf);
render_ingredients(self.row_index, ingredients, 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().copied();
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,206 @@
use itertools::Itertools;
use ratatui::{
buffer::Buffer,
layout::{Alignment, Constraint, Layout, Margin, Rect},
style::{Styled, Stylize},
symbols::Marker,
widgets::{
canvas::{self, Canvas, Map, MapResolution, Points},
Block, BorderType, Clear, Padding, Row, Scrollbar, ScrollbarOrientation, ScrollbarState,
Sparkline, StatefulWidget, Table, TableState, Widget,
},
};
use crate::{RgbSwatch, THEME};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct TracerouteTab {
row_index: usize,
}
impl TracerouteTab {
/// Select the previous row (with wrap around).
pub fn prev_row(&mut self) {
self.row_index = self.row_index.saturating_add(HOPS.len() - 1) % HOPS.len();
}
/// Select the next row (with wrap around).
pub fn next_row(&mut self) {
self.row_index = self.row_index.saturating_add(1) % 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 horizontal = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
let vertical = Layout::vertical([Constraint::Min(0), Constraint::Length(3)]);
let [left, map] = horizontal.areas(area);
let [hops, pings] = vertical.areas(left);
render_hops(self.row_index, hops, buf);
render_ping(self.row_index, pings, buf);
render_map(self.row_index, map, 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]));
let block = Block::new()
.padding(Padding::new(1, 1, 1, 1))
.title_alignment(Alignment::Center)
.title("Traceroute bad.horse".bold().white());
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: 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,164 @@
use itertools::Itertools;
use palette::Okhsv;
use ratatui::{
buffer::Buffer,
layout::{Constraint, Direction, Layout, Margin, Rect},
style::{Color, Style, Stylize},
symbols,
widgets::{
calendar::{CalendarEventStore, Monthly},
Bar, BarChart, BarGroup, Block, Clear, LineGauge, Padding, Widget,
},
};
use time::OffsetDateTime;
use crate::{color_from_oklab, RgbSwatch, THEME};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct WeatherTab {
pub download_progress: usize,
}
impl WeatherTab {
/// Simulate a download indicator by decrementing the row index.
pub fn prev(&mut self) {
self.download_progress = self.download_progress.saturating_sub(1);
}
/// Simulate a download indicator by incrementing the row index.
pub fn next(&mut self) {
self.download_progress = self.download_progress.saturating_add(1);
}
}
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 [main, _, gauges] = Layout::vertical([
Constraint::Min(0),
Constraint::Length(1),
Constraint::Length(1),
])
.areas(area);
let [calendar, charts] =
Layout::horizontal([Constraint::Length(23), Constraint::Min(0)]).areas(main);
let [simple, horizontal] =
Layout::vertical([Constraint::Length(29), Constraint::Min(0)]).areas(charts);
render_calendar(calendar, buf);
render_simple_barchart(simple, buf);
render_horizontal_barchart(horizontal, buf);
render_gauge(self.download_progress, gauges, buf);
}
}
fn render_calendar(area: Rect, buf: &mut Buffer) {
let date = OffsetDateTime::now_utc().date();
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/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);
}
#[allow(clippy::cast_precision_loss)]
pub fn render_gauge(progress: usize, area: Rect, buf: &mut Buffer) {
let percent = (progress * 3).min(100) as f64;
render_line_gauge(percent, area, buf);
}
#[allow(clippy::cast_possible_truncation)]
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 filled_color = color_from_oklab(hue, Okhsv::max_saturation(), value);
let unfilled_color = 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())
.filled_style(Style::new().fg(filled_color))
.unfilled_style(Style::new().fg(unfilled_color))
.line_set(symbols::line::THICK)
.render(area, buf);
}

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

@@ -0,0 +1,136 @@
use ratatui::style::{Color, Modifier, Style};
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::Rgb(215, 0, 0);
const BLACK: Color = Color::Rgb(8, 8, 8); // not really black, often #080808
const DARK_GRAY: Color = Color::Rgb(68, 68, 68);
const MID_GRAY: Color = Color::Rgb(128, 128, 128);
const LIGHT_GRAY: Color = Color::Rgb(188, 188, 188);
const WHITE: Color = Color::Rgb(238, 238, 238); // not really white, often #eeeeee

127
examples/docsrs.rs Normal file
View File

@@ -0,0 +1,127 @@
//! # [Ratatui] Docs.rs example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use color_eyre::Result;
use ratatui::{
crossterm::event::{self, Event, KeyCode},
layout::{Constraint, Layout},
style::{Color, Modifier, Style, Stylize},
text::{Line, Span, Text},
widgets::{Block, Borders, Paragraph},
DefaultTerminal, Frame,
};
/// Example code for lib.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() -> Result<()> {
color_eyre::install()?;
let first_arg = std::env::args().nth(1).unwrap_or_default();
let terminal = ratatui::init();
let app_result = run(terminal, &first_arg);
ratatui::restore();
app_result
}
fn run(mut terminal: DefaultTerminal, first_arg: &str) -> Result<()> {
let mut should_quit = false;
while !should_quit {
terminal.draw(match first_arg {
"layout" => layout,
"styling" => styling,
_ => hello_world,
})?;
should_quit = handle_events()?;
}
Ok(())
}
fn handle_events() -> std::io::Result<bool> {
if let Event::Key(key) = event::read()? {
if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(true);
}
}
Ok(false)
}
fn hello_world(frame: &mut Frame) {
frame.render_widget(
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
frame.area(),
);
}
fn layout(frame: &mut Frame) {
let vertical = Layout::vertical([
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
]);
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2); 2]);
let [title_bar, main_area, status_bar] = vertical.areas(frame.area());
let [left, right] = horizontal.areas(main_area);
frame.render_widget(
Block::new().borders(Borders::TOP).title("Title Bar"),
title_bar,
);
frame.render_widget(
Block::new().borders(Borders::TOP).title("Status Bar"),
status_bar,
);
frame.render_widget(Block::bordered().title("Left"), left);
frame.render_widget(Block::bordered().title("Right"), right);
}
fn styling(frame: &mut Frame) {
let areas = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
])
.split(frame.area());
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]);
}

530
examples/flex.rs Normal file
View File

@@ -0,0 +1,530 @@
//! # [Ratatui] Flex example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use std::num::NonZeroUsize;
use color_eyre::Result;
use ratatui::{
buffer::Buffer,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{
Alignment,
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
Flex, Layout, Rect,
},
style::{palette::tailwind, Color, Modifier, Style, Stylize},
symbols::{self, line},
text::{Line, Text},
widgets::{
Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Tabs,
Widget,
},
DefaultTerminal,
};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::default().run(terminal);
ratatui::restore();
app_result
}
const EXAMPLE_DATA: &[(&str, &[Constraint])] = &[
(
"Min(u16) takes any excess space always",
&[Length(10), Min(10), Max(10), Percentage(10), Ratio(1,10)],
),
(
"Fill(u16) takes any excess space always",
&[Length(20), Percentage(20), Ratio(1, 5), Fill(1)],
),
(
"Here's all constraints in one line",
&[Length(10), Min(10), Max(10), Percentage(10), Ratio(1,10), Fill(1)],
),
(
"",
&[Max(50), Min(50)],
),
(
"",
&[Max(20), Length(10)],
),
(
"",
&[Max(20), Length(10)],
),
(
"Min grows always but also allows Fill to grow",
&[Percentage(50), Fill(1), Fill(2), Min(50)],
),
(
"In `Legacy`, the last constraint of lowest priority takes excess space",
&[Length(20), Length(20), Percentage(20)],
),
("", &[Length(20), Percentage(20), Length(20)]),
("A lowest priority constraint will be broken before a high priority constraint", &[Ratio(1,4), Percentage(20)]),
("`Length` is higher priority than `Percentage`", &[Percentage(20), Length(10)]),
("`Min/Max` is higher priority than `Length`", &[Length(10), Max(20)]),
("", &[Length(100), Min(20)]),
("`Length` is higher priority than `Min/Max`", &[Max(20), Length(10)]),
("", &[Min(20), Length(90)]),
("Fill is the lowest priority and will fill any excess space", &[Fill(1), Ratio(1, 4)]),
("Fill can be used to scale proportionally with other Fill blocks", &[Fill(1), Percentage(20), Fill(2)]),
("", &[Ratio(1, 3), Percentage(20), Ratio(2, 3)]),
("Legacy will stretch the last lowest priority constraint\nStretch will only stretch equal weighted constraints", &[Length(20), Length(15)]),
("", &[Percentage(20), Length(15)]),
("`Fill(u16)` fills up excess space, but is lower priority to spacers.\ni.e. Fill will only have widths in Flex::Stretch and Flex::Legacy", &[Fill(1), Fill(1)]),
("", &[Length(20), Length(20)]),
(
"When not using `Flex::Stretch` or `Flex::Legacy`,\n`Min(u16)` and `Max(u16)` collapse to their lowest values",
&[Min(20), Max(20)],
),
(
"",
&[Max(20)],
),
("", &[Min(20), Max(20), Length(20), Length(20)]),
("", &[Fill(0), Fill(0)]),
(
"`Fill(1)` can be to scale with respect to other `Fill(2)`",
&[Fill(1), Fill(2)],
),
(
"",
&[Fill(1), Min(10), Max(10), Fill(2)],
),
(
"`Fill(0)` collapses if there are other non-zero `Fill(_)`\nconstraints. e.g. `[Fill(0), Fill(0), Fill(1)]`:",
&[
Fill(0),
Fill(0),
Fill(1),
],
),
];
#[derive(Default, Clone, Copy)]
struct App {
selected_tab: SelectedTab,
scroll_offset: u16,
spacing: u16,
state: AppState,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum AppState {
#[default]
Running,
Quit,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Example {
constraints: Vec<Constraint>,
description: String,
flex: Flex,
spacing: u16,
}
/// Tabs for the different layouts
///
/// Note: the order of the variants will determine the order of the tabs this uses several derive
/// macros from the `strum` crate to make it easier to iterate over the variants.
/// (`FromRepr`,`Display`,`EnumIter`).
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, FromRepr, Display, EnumIter)]
enum SelectedTab {
#[default]
Legacy,
Start,
Center,
End,
SpaceAround,
SpaceBetween,
}
impl App {
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
// increase the layout cache to account for the number of layout events. This ensures that
// layout is not generally reprocessed on every frame (which would lead to possible janky
// results when there are more than one possible solution to the requested layout). This
// assumes the user changes spacing about a 100 times or so.
let cache_size = EXAMPLE_DATA.len() * SelectedTab::iter().len() * 100;
Layout::init_cache(NonZeroUsize::new(cache_size).unwrap());
while self.is_running() {
terminal.draw(|frame| frame.render_widget(self, frame.area()))?;
self.handle_events()?;
}
Ok(())
}
fn is_running(self) -> bool {
self.state == AppState::Running
}
fn handle_events(&mut self) -> Result<()> {
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
KeyCode::Char('l') | KeyCode::Right => self.next(),
KeyCode::Char('h') | KeyCode::Left => self.previous(),
KeyCode::Char('j') | KeyCode::Down => self.down(),
KeyCode::Char('k') | KeyCode::Up => self.up(),
KeyCode::Char('g') | KeyCode::Home => self.top(),
KeyCode::Char('G') | KeyCode::End => self.bottom(),
KeyCode::Char('+') => self.increment_spacing(),
KeyCode::Char('-') => self.decrement_spacing(),
_ => (),
},
_ => {}
}
Ok(())
}
fn next(&mut self) {
self.selected_tab = self.selected_tab.next();
}
fn previous(&mut self) {
self.selected_tab = self.selected_tab.previous();
}
fn up(&mut self) {
self.scroll_offset = self.scroll_offset.saturating_sub(1);
}
fn down(&mut self) {
self.scroll_offset = self
.scroll_offset
.saturating_add(1)
.min(max_scroll_offset());
}
fn top(&mut self) {
self.scroll_offset = 0;
}
fn bottom(&mut self) {
self.scroll_offset = max_scroll_offset();
}
fn increment_spacing(&mut self) {
self.spacing = self.spacing.saturating_add(1);
}
fn decrement_spacing(&mut self) {
self.spacing = self.spacing.saturating_sub(1);
}
fn quit(&mut self) {
self.state = AppState::Quit;
}
}
// when scrolling, make sure we don't scroll past the last example
fn max_scroll_offset() -> u16 {
example_height()
- EXAMPLE_DATA
.last()
.map_or(0, |(desc, _)| get_description_height(desc) + 4)
}
/// The height of all examples combined
///
/// Each may or may not have a title so we need to account for that.
fn example_height() -> u16 {
EXAMPLE_DATA
.iter()
.map(|(desc, _)| get_description_height(desc) + 4)
.sum()
}
impl Widget for App {
fn render(self, area: Rect, buf: &mut Buffer) {
let layout = Layout::vertical([Length(3), Length(1), Fill(0)]);
let [tabs, axis, demo] = layout.areas(area);
self.tabs().render(tabs, buf);
let scroll_needed = self.render_demo(demo, buf);
let axis_width = if scroll_needed {
axis.width.saturating_sub(1)
} else {
axis.width
};
Self::axis(axis_width, self.spacing).render(axis, buf);
}
}
impl App {
fn tabs(self) -> impl Widget {
let tab_titles = SelectedTab::iter().map(SelectedTab::to_tab_title);
let block = Block::new()
.title("Flex Layouts ".bold())
.title(" Use ◄ ► to change tab, ▲ ▼ to scroll, - + to change spacing ");
Tabs::new(tab_titles)
.block(block)
.highlight_style(Modifier::REVERSED)
.select(self.selected_tab as usize)
.divider(" ")
.padding("", "")
}
/// a bar like `<----- 80 px (gap: 2 px)? ----->`
fn axis(width: u16, spacing: u16) -> impl Widget {
let width = width as usize;
// only show gap when spacing is not zero
let label = if spacing != 0 {
format!("{width} px (gap: {spacing} px)")
} else {
format!("{width} px")
};
let bar_width = width.saturating_sub(2); // we want to `<` and `>` at the ends
let width_bar = format!("<{label:-^bar_width$}>");
Paragraph::new(width_bar.dark_gray()).centered()
}
/// Render the demo content
///
/// This function renders the demo content into a separate buffer and then splices the buffer
/// into the main buffer. This is done to make it possible to handle scrolling easily.
///
/// Returns bool indicating whether scroll was needed
#[allow(clippy::cast_possible_truncation)]
fn render_demo(self, area: Rect, buf: &mut Buffer) -> bool {
// render demo content into a separate buffer so all examples fit we add an extra
// area.height to make sure the last example is fully visible even when the scroll offset is
// at the max
let height = example_height();
let demo_area = Rect::new(0, 0, area.width, height);
let mut demo_buf = Buffer::empty(demo_area);
let scrollbar_needed = self.scroll_offset != 0 || height > area.height;
let content_area = if scrollbar_needed {
Rect {
width: demo_area.width - 1,
..demo_area
}
} else {
demo_area
};
let mut spacing = self.spacing;
self.selected_tab
.render(content_area, &mut demo_buf, &mut spacing);
let visible_content = demo_buf
.content
.into_iter()
.skip((area.width * self.scroll_offset) as usize)
.take(area.area() as usize);
for (i, cell) in visible_content.enumerate() {
let x = i as u16 % area.width;
let y = i as u16 / area.width;
buf[(area.x + x, area.y + y)] = cell;
}
if scrollbar_needed {
let area = area.intersection(buf.area);
let mut state = ScrollbarState::new(max_scroll_offset() as usize)
.position(self.scroll_offset as usize);
Scrollbar::new(ScrollbarOrientation::VerticalRight).render(area, buf, &mut state);
}
scrollbar_needed
}
}
impl SelectedTab {
/// Get the previous tab, if there is no previous tab return the current tab.
fn previous(self) -> Self {
let current_index: usize = self as usize;
let previous_index = current_index.saturating_sub(1);
Self::from_repr(previous_index).unwrap_or(self)
}
/// Get the next tab, if there is no next tab return the current tab.
fn next(self) -> Self {
let current_index = self as usize;
let next_index = current_index.saturating_add(1);
Self::from_repr(next_index).unwrap_or(self)
}
/// Convert a `SelectedTab` into a `Line` to display it by the `Tabs` widget.
fn to_tab_title(value: Self) -> Line<'static> {
use tailwind::{INDIGO, ORANGE, SKY};
let text = value.to_string();
let color = match value {
Self::Legacy => ORANGE.c400,
Self::Start => SKY.c400,
Self::Center => SKY.c300,
Self::End => SKY.c200,
Self::SpaceAround => INDIGO.c400,
Self::SpaceBetween => INDIGO.c300,
};
format!(" {text} ").fg(color).bg(Color::Black).into()
}
}
impl StatefulWidget for SelectedTab {
type State = u16;
fn render(self, area: Rect, buf: &mut Buffer, spacing: &mut Self::State) {
let spacing = *spacing;
match self {
Self::Legacy => Self::render_examples(area, buf, Flex::Legacy, spacing),
Self::Start => Self::render_examples(area, buf, Flex::Start, spacing),
Self::Center => Self::render_examples(area, buf, Flex::Center, spacing),
Self::End => Self::render_examples(area, buf, Flex::End, spacing),
Self::SpaceAround => Self::render_examples(area, buf, Flex::SpaceAround, spacing),
Self::SpaceBetween => Self::render_examples(area, buf, Flex::SpaceBetween, spacing),
}
}
}
impl SelectedTab {
fn render_examples(area: Rect, buf: &mut Buffer, flex: Flex, spacing: u16) {
let heights = EXAMPLE_DATA
.iter()
.map(|(desc, _)| get_description_height(desc) + 4);
let areas = Layout::vertical(heights).flex(Flex::Start).split(area);
for (area, (description, constraints)) in areas.iter().zip(EXAMPLE_DATA.iter()) {
Example::new(constraints, description, flex, spacing).render(*area, buf);
}
}
}
impl Example {
fn new(constraints: &[Constraint], description: &str, flex: Flex, spacing: u16) -> Self {
Self {
constraints: constraints.into(),
description: description.into(),
flex,
spacing,
}
}
}
impl Widget for Example {
fn render(self, area: Rect, buf: &mut Buffer) {
let title_height = get_description_height(&self.description);
let layout = Layout::vertical([Length(title_height), Fill(0)]);
let [title, illustrations] = layout.areas(area);
let (blocks, spacers) = Layout::horizontal(&self.constraints)
.flex(self.flex)
.spacing(self.spacing)
.split_with_spacers(illustrations);
if !self.description.is_empty() {
Paragraph::new(
self.description
.split('\n')
.map(|s| format!("// {s}").italic().fg(tailwind::SLATE.c400))
.map(Line::from)
.collect::<Vec<Line>>(),
)
.render(title, buf);
}
for (block, constraint) in blocks.iter().zip(&self.constraints) {
Self::illustration(*constraint, block.width).render(*block, buf);
}
for spacer in spacers.iter() {
Self::render_spacer(*spacer, buf);
}
}
}
impl Example {
fn render_spacer(spacer: Rect, buf: &mut Buffer) {
if spacer.width > 1 {
let corners_only = symbols::border::Set {
top_left: line::NORMAL.top_left,
top_right: line::NORMAL.top_right,
bottom_left: line::NORMAL.bottom_left,
bottom_right: line::NORMAL.bottom_right,
vertical_left: " ",
vertical_right: " ",
horizontal_top: " ",
horizontal_bottom: " ",
};
Block::bordered()
.border_set(corners_only)
.border_style(Style::reset().dark_gray())
.render(spacer, buf);
} else {
Paragraph::new(Text::from(vec![
Line::from(""),
Line::from(""),
Line::from(""),
Line::from(""),
]))
.style(Style::reset().dark_gray())
.render(spacer, buf);
}
let width = spacer.width;
let label = if width > 4 {
format!("{width} px")
} else if width > 2 {
format!("{width}")
} else {
String::new()
};
let text = Text::from(vec![
Line::raw(""),
Line::raw(""),
Line::styled(label, Style::reset().dark_gray()),
]);
Paragraph::new(text)
.style(Style::reset().dark_gray())
.alignment(Alignment::Center)
.render(spacer, buf);
}
fn illustration(constraint: Constraint, width: u16) -> impl Widget {
let main_color = color_for_constraint(constraint);
let fg_color = Color::White;
let title = format!("{constraint}");
let content = format!("{width} px");
let text = format!("{title}\n{content}");
let block = Block::bordered()
.border_set(symbols::border::QUADRANT_OUTSIDE)
.border_style(Style::reset().fg(main_color).reversed())
.style(Style::default().fg(fg_color).bg(main_color));
Paragraph::new(text).centered().block(block)
}
}
const fn color_for_constraint(constraint: Constraint) -> Color {
use tailwind::{BLUE, SLATE};
match constraint {
Constraint::Min(_) => BLUE.c900,
Constraint::Max(_) => BLUE.c800,
Constraint::Length(_) => SLATE.c700,
Constraint::Percentage(_) => SLATE.c800,
Constraint::Ratio(_, _) => SLATE.c900,
Constraint::Fill(_) => SLATE.c950,
}
}
#[allow(clippy::cast_possible_truncation)]
fn get_description_height(s: &str) -> u16 {
if s.is_empty() {
0
} else {
s.split('\n').count() as u16
}
}

View File

@@ -1,123 +1,205 @@
#[allow(dead_code)]
mod util;
//! # [Ratatui] Gauge example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use crate::util::event::{Event, Events};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Gauge},
Terminal,
use std::time::Duration;
use color_eyre::Result;
use ratatui::{
buffer::Buffer,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Alignment, Constraint, Layout, Rect},
style::{palette::tailwind, Color, Style, Stylize},
text::{Line, Span},
widgets::{Block, Borders, Gauge, Padding, Paragraph, Widget},
DefaultTerminal,
};
const GAUGE1_COLOR: Color = tailwind::RED.c800;
const GAUGE2_COLOR: Color = tailwind::GREEN.c800;
const GAUGE3_COLOR: Color = tailwind::BLUE.c800;
const GAUGE4_COLOR: Color = tailwind::ORANGE.c800;
const CUSTOM_LABEL_COLOR: Color = tailwind::SLATE.c200;
#[derive(Debug, Default, Clone, Copy)]
struct App {
state: AppState,
progress_columns: u16,
progress1: u16,
progress2: u16,
progress2: f64,
progress3: f64,
progress4: u16,
progress4: f64,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum AppState {
#[default]
Running,
Started,
Quitting,
}
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::default().run(terminal);
ratatui::restore();
app_result
}
impl App {
fn new() -> App {
App {
progress1: 0,
progress2: 0,
progress3: 0.0,
progress4: 0,
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
while self.state != AppState::Quitting {
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
self.handle_events()?;
self.update(terminal.size()?.width);
}
Ok(())
}
fn update(&mut self) {
self.progress1 += 5;
if self.progress1 > 100 {
self.progress1 = 0;
}
self.progress2 += 10;
if self.progress2 > 100 {
self.progress2 = 0;
}
self.progress3 += 0.001;
if self.progress3 > 1.0 {
self.progress3 = 0.0;
}
self.progress4 += 3;
if self.progress4 > 100 {
self.progress4 = 0;
fn update(&mut self, terminal_width: u16) {
if self.state != AppState::Started {
return;
}
// progress1 and progress2 help show the difference between ratio and percentage measuring
// the same thing, but converting to either a u16 or f64. Effectively, we're showing the
// difference between how a continuous gauge acts for floor and rounded values.
self.progress_columns = (self.progress_columns + 1).clamp(0, terminal_width);
self.progress1 = self.progress_columns * 100 / terminal_width;
self.progress2 = f64::from(self.progress_columns) * 100.0 / f64::from(terminal_width);
// progress3 and progress4 similarly show the difference between unicode and non-unicode
// gauges measuring the same thing.
self.progress3 = (self.progress3 + 0.1).clamp(40.0, 100.0);
self.progress4 = (self.progress4 + 0.1).clamp(40.0, 100.0);
}
}
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let events = Events::new();
let mut app = App::new();
loop {
terminal.draw(|mut f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
]
.as_ref(),
)
.split(f.size());
let gauge = Gauge::default()
.block(Block::default().title("Gauge1").borders(Borders::ALL))
.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))
.style(Style::default().fg(Color::Magenta).bg(Color::Green))
.percent(app.progress2)
.label(&label);
f.render_widget(gauge, chunks[1]);
let gauge = Gauge::default()
.block(Block::default().title("Gauge3").borders(Borders::ALL))
.style(Style::default().fg(Color::Yellow))
.ratio(app.progress3);
f.render_widget(gauge, chunks[2]);
let label = format!("{}/100", app.progress2);
let gauge = Gauge::default()
.block(Block::default().title("Gauge4").borders(Borders::ALL))
.style(Style::default().fg(Color::Cyan).modifier(Modifier::ITALIC))
.percent(app.progress4)
.label(&label);
f.render_widget(gauge, chunks[3]);
})?;
match events.next()? {
Event::Input(input) => {
if input == Key::Char('q') {
break;
fn handle_events(&mut self) -> Result<()> {
let timeout = Duration::from_secs_f32(1.0 / 20.0);
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char(' ') | KeyCode::Enter => self.start(),
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
_ => {}
}
}
}
Event::Tick => {
app.update();
}
}
Ok(())
}
Ok(())
fn start(&mut self) {
self.state = AppState::Started;
}
fn quit(&mut self) {
self.state = AppState::Quitting;
}
}
impl Widget for &App {
#[allow(clippy::similar_names)]
fn render(self, area: Rect, buf: &mut Buffer) {
use Constraint::{Length, Min, Ratio};
let layout = Layout::vertical([Length(2), Min(0), Length(1)]);
let [header_area, gauge_area, footer_area] = layout.areas(area);
let layout = Layout::vertical([Ratio(1, 4); 4]);
let [gauge1_area, gauge2_area, gauge3_area, gauge4_area] = layout.areas(gauge_area);
render_header(header_area, buf);
render_footer(footer_area, buf);
self.render_gauge1(gauge1_area, buf);
self.render_gauge2(gauge2_area, buf);
self.render_gauge3(gauge3_area, buf);
self.render_gauge4(gauge4_area, buf);
}
}
fn render_header(area: Rect, buf: &mut Buffer) {
Paragraph::new("Ratatui Gauge Example")
.bold()
.alignment(Alignment::Center)
.fg(CUSTOM_LABEL_COLOR)
.render(area, buf);
}
fn render_footer(area: Rect, buf: &mut Buffer) {
Paragraph::new("Press ENTER to start")
.alignment(Alignment::Center)
.fg(CUSTOM_LABEL_COLOR)
.bold()
.render(area, buf);
}
impl App {
fn render_gauge1(&self, area: Rect, buf: &mut Buffer) {
let title = title_block("Gauge with percentage");
Gauge::default()
.block(title)
.gauge_style(GAUGE1_COLOR)
.percent(self.progress1)
.render(area, buf);
}
fn render_gauge2(&self, area: Rect, buf: &mut Buffer) {
let title = title_block("Gauge with ratio and custom label");
let label = Span::styled(
format!("{:.1}/100", self.progress2),
Style::new().italic().bold().fg(CUSTOM_LABEL_COLOR),
);
Gauge::default()
.block(title)
.gauge_style(GAUGE2_COLOR)
.ratio(self.progress2 / 100.0)
.label(label)
.render(area, buf);
}
fn render_gauge3(&self, area: Rect, buf: &mut Buffer) {
let title = title_block("Gauge with ratio (no unicode)");
let label = format!("{:.1}%", self.progress3);
Gauge::default()
.block(title)
.gauge_style(GAUGE3_COLOR)
.ratio(self.progress3 / 100.0)
.label(label)
.render(area, buf);
}
fn render_gauge4(&self, area: Rect, buf: &mut Buffer) {
let title = title_block("Gauge with ratio (unicode)");
let label = format!("{:.1}%", self.progress3);
Gauge::default()
.block(title)
.gauge_style(GAUGE4_COLOR)
.ratio(self.progress4 / 100.0)
.label(label)
.use_unicode(true)
.render(area, buf);
}
}
fn title_block(title: &str) -> Block {
let title = Line::from(title).centered();
Block::new()
.borders(Borders::NONE)
.padding(Padding::vertical(1))
.title(title)
.fg(CUSTOM_LABEL_COLOR)
}

72
examples/hello_world.rs Normal file
View File

@@ -0,0 +1,72 @@
//! # [Ratatui] Hello World example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use std::time::Duration;
use color_eyre::{eyre::Context, Result};
use ratatui::{
crossterm::event::{self, Event, KeyCode},
widgets::Paragraph,
DefaultTerminal, Frame,
};
/// This is a bare minimum example. There are many approaches to running an application loop, so
/// this is not meant to be prescriptive. It is only meant to demonstrate the basic setup and
/// teardown of a terminal application.
///
/// This example does not handle events or update the application state. It just draws a greeting
/// and exits when the user presses 'q'.
fn main() -> Result<()> {
color_eyre::install()?; // augment errors / panics with easy to read messages
let terminal = ratatui::init();
let app_result = run(terminal).context("app loop failed");
ratatui::restore();
app_result
}
/// Run the application loop. This is where you would handle events and update the application
/// state. This example exits when the user presses 'q'. Other styles of application loops are
/// possible, for example, you could have multiple application states and switch between them based
/// on events, or you could have a single application state and update it based on events.
fn run(mut terminal: DefaultTerminal) -> Result<()> {
loop {
terminal.draw(draw)?;
if should_quit()? {
break;
}
}
Ok(())
}
/// Render the application. This is where you would draw the application UI. This example draws a
/// greeting.
fn draw(frame: &mut Frame) {
let greeting = Paragraph::new("Hello World! (press 'q' to quit)");
frame.render_widget(greeting, frame.area());
}
/// Check if the user has pressed 'q'. This is where you would handle events. This example just
/// checks if the user has pressed 'q' and returns true if they have. It does not handle any other
/// events. There is a 250ms timeout on the event poll to ensure that the terminal is rendered at
/// least once every 250ms. This allows you to do other work in the application loop, such as
/// updating the application state, without blocking the event loop for too long.
fn should_quit() -> Result<bool> {
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
if let Event::Key(key) = event::read().context("event read failed")? {
return Ok(KeyCode::Char('q') == key.code);
}
}
Ok(false)
}

101
examples/hyperlink.rs Normal file
View File

@@ -0,0 +1,101 @@
//! # [Ratatui] Hyperlink examplew
//!
//! Shows how to use [OSC 8] to create hyperlinks in the terminal.
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [OSC 8]: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use color_eyre::Result;
use itertools::Itertools;
use ratatui::{
buffer::Buffer,
crossterm::event::{self, Event, KeyCode},
layout::Rect,
style::Stylize,
text::{Line, Text},
widgets::Widget,
DefaultTerminal,
};
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::new().run(terminal);
ratatui::restore();
app_result
}
struct App {
hyperlink: Hyperlink<'static>,
}
impl App {
fn new() -> Self {
let text = Line::from(vec!["Example ".into(), "hyperlink".blue()]);
let hyperlink = Hyperlink::new(text, "https://example.com");
Self { hyperlink }
}
fn run(self, mut terminal: DefaultTerminal) -> Result<()> {
loop {
terminal.draw(|frame| frame.render_widget(&self.hyperlink, frame.area()))?;
if let Event::Key(key) = event::read()? {
if matches!(key.code, KeyCode::Char('q') | KeyCode::Esc) {
break;
}
}
}
Ok(())
}
}
/// A hyperlink widget that renders a hyperlink in the terminal using [OSC 8].
///
/// [OSC 8]: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
struct Hyperlink<'content> {
text: Text<'content>,
url: String,
}
impl<'content> Hyperlink<'content> {
fn new(text: impl Into<Text<'content>>, url: impl Into<String>) -> Self {
Self {
text: text.into(),
url: url.into(),
}
}
}
impl Widget for &Hyperlink<'_> {
fn render(self, area: Rect, buffer: &mut Buffer) {
(&self.text).render(area, buffer);
// this is a hacky workaround for https://github.com/ratatui/ratatui/issues/902, a bug
// in the terminal code that incorrectly calculates the width of ANSI escape sequences. It
// works by rendering the hyperlink as a series of 2-character chunks, which is the
// calculated width of the hyperlink text.
for (i, two_chars) in self
.text
.to_string()
.chars()
.chunks(2)
.into_iter()
.enumerate()
{
let text = two_chars.collect::<String>();
let hyperlink = format!("\x1B]8;;{}\x07{}\x1B]8;;\x07", self.url, text);
buffer[(area.x + i as u16 * 2, area.y)].set_symbol(hyperlink.as_str());
}
}
}

292
examples/inline.rs Normal file
View File

@@ -0,0 +1,292 @@
//! # [Ratatui] Inline example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use std::{
collections::{BTreeMap, VecDeque},
sync::mpsc,
thread,
time::{Duration, Instant},
};
use color_eyre::Result;
use rand::distributions::{Distribution, Uniform};
use ratatui::{
backend::Backend,
crossterm::event,
layout::{Constraint, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
text::{Line, Span},
widgets::{Block, Gauge, LineGauge, List, ListItem, Paragraph, Widget},
Frame, Terminal, TerminalOptions, Viewport,
};
fn main() -> Result<()> {
color_eyre::install()?;
let mut terminal = ratatui::init_with_options(TerminalOptions {
viewport: Viewport::Inline(8),
});
let (tx, rx) = mpsc::channel();
input_handling(tx.clone());
let workers = workers(tx);
let mut downloads = downloads();
for w in &workers {
let d = downloads.next(w.id).unwrap();
w.tx.send(d).unwrap();
}
let app_result = run(&mut terminal, workers, downloads, rx);
ratatui::restore();
app_result
}
const NUM_DOWNLOADS: usize = 10;
type DownloadId = usize;
type WorkerId = usize;
enum Event {
Input(event::KeyEvent),
Tick,
Resize,
DownloadUpdate(WorkerId, DownloadId, f64),
DownloadDone(WorkerId, DownloadId),
}
struct Downloads {
pending: VecDeque<Download>,
in_progress: BTreeMap<WorkerId, DownloadInProgress>,
}
impl Downloads {
fn next(&mut self, worker_id: WorkerId) -> Option<Download> {
match self.pending.pop_front() {
Some(d) => {
self.in_progress.insert(
worker_id,
DownloadInProgress {
id: d.id,
started_at: Instant::now(),
progress: 0.0,
},
);
Some(d)
}
None => None,
}
}
}
struct DownloadInProgress {
id: DownloadId,
started_at: Instant,
progress: f64,
}
struct Download {
id: DownloadId,
size: usize,
}
struct Worker {
id: WorkerId,
tx: mpsc::Sender<Download>,
}
fn input_handling(tx: mpsc::Sender<Event>) {
let tick_rate = Duration::from_millis(200);
thread::spawn(move || {
let mut last_tick = Instant::now();
loop {
// poll for tick rate duration, if no events, sent tick event.
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout).unwrap() {
match event::read().unwrap() {
event::Event::Key(key) => tx.send(Event::Input(key)).unwrap(),
event::Event::Resize(_, _) => tx.send(Event::Resize).unwrap(),
_ => {}
};
}
if last_tick.elapsed() >= tick_rate {
tx.send(Event::Tick).unwrap();
last_tick = Instant::now();
}
}
});
}
#[allow(clippy::cast_precision_loss, clippy::needless_pass_by_value)]
fn workers(tx: mpsc::Sender<Event>) -> Vec<Worker> {
(0..4)
.map(|id| {
let (worker_tx, worker_rx) = mpsc::channel::<Download>();
let tx = tx.clone();
thread::spawn(move || {
while let Ok(download) = worker_rx.recv() {
let mut remaining = download.size;
while remaining > 0 {
let wait = (remaining as u64).min(10);
thread::sleep(Duration::from_millis(wait * 10));
remaining = remaining.saturating_sub(10);
let progress = (download.size - remaining) * 100 / download.size;
tx.send(Event::DownloadUpdate(id, download.id, progress as f64))
.unwrap();
}
tx.send(Event::DownloadDone(id, download.id)).unwrap();
}
});
Worker { id, tx: worker_tx }
})
.collect()
}
fn downloads() -> Downloads {
let distribution = Uniform::new(0, 1000);
let mut rng = rand::thread_rng();
let pending = (0..NUM_DOWNLOADS)
.map(|id| {
let size = distribution.sample(&mut rng);
Download { id, size }
})
.collect();
Downloads {
pending,
in_progress: BTreeMap::new(),
}
}
#[allow(clippy::needless_pass_by_value)]
fn run(
terminal: &mut Terminal<impl Backend>,
workers: Vec<Worker>,
mut downloads: Downloads,
rx: mpsc::Receiver<Event>,
) -> Result<()> {
let mut redraw = true;
loop {
if redraw {
terminal.draw(|frame| draw(frame, &downloads))?;
}
redraw = true;
match rx.recv()? {
Event::Input(event) => {
if event.code == event::KeyCode::Char('q') {
break;
}
}
Event::Resize => {
terminal.autoresize()?;
}
Event::Tick => {}
Event::DownloadUpdate(worker_id, _download_id, progress) => {
let download = downloads.in_progress.get_mut(&worker_id).unwrap();
download.progress = progress;
redraw = false;
}
Event::DownloadDone(worker_id, download_id) => {
let download = downloads.in_progress.remove(&worker_id).unwrap();
terminal.insert_before(1, |buf| {
Paragraph::new(Line::from(vec![
Span::from("Finished "),
Span::styled(
format!("download {download_id}"),
Style::default().add_modifier(Modifier::BOLD),
),
Span::from(format!(
" in {}ms",
download.started_at.elapsed().as_millis()
)),
]))
.render(buf.area, buf);
})?;
match downloads.next(worker_id) {
Some(d) => workers[worker_id].tx.send(d).unwrap(),
None => {
if downloads.in_progress.is_empty() {
terminal.insert_before(1, |buf| {
Paragraph::new("Done !").render(buf.area, buf);
})?;
break;
}
}
};
}
};
}
Ok(())
}
fn draw(frame: &mut Frame, downloads: &Downloads) {
let area = frame.area();
let block = Block::new().title(Line::from("Progress").centered());
frame.render_widget(block, area);
let vertical = Layout::vertical([Constraint::Length(2), Constraint::Length(4)]).margin(1);
let horizontal = Layout::horizontal([Constraint::Percentage(20), Constraint::Percentage(80)]);
let [progress_area, main] = vertical.areas(area);
let [list_area, gauge_area] = horizontal.areas(main);
// total progress
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
#[allow(clippy::cast_precision_loss)]
let progress = LineGauge::default()
.filled_style(Style::default().fg(Color::Blue))
.label(format!("{done}/{NUM_DOWNLOADS}"))
.ratio(done as f64 / NUM_DOWNLOADS as f64);
frame.render_widget(progress, progress_area);
// in progress downloads
let items: Vec<ListItem> = downloads
.in_progress
.values()
.map(|download| {
ListItem::new(Line::from(vec![
Span::raw(symbols::DOT),
Span::styled(
format!(" download {:>2}", download.id),
Style::default()
.fg(Color::LightGreen)
.add_modifier(Modifier::BOLD),
),
Span::raw(format!(
" ({}ms)",
download.started_at.elapsed().as_millis()
)),
]))
})
.collect();
let list = List::new(items);
frame.render_widget(list, list_area);
#[allow(clippy::cast_possible_truncation)]
for (i, (_, download)) in downloads.in_progress.iter().enumerate() {
let gauge = Gauge::default()
.gauge_style(Style::default().fg(Color::Yellow))
.ratio(download.progress / 100.0);
if gauge_area.top().saturating_add(i as u16) > area.bottom() {
continue;
}
frame.render_widget(
gauge,
Rect {
x: gauge_area.left(),
y: gauge_area.top().saturating_add(i as u16),
width: gauge_area.width,
height: 1,
},
);
}
}

View File

@@ -1,56 +1,188 @@
#[allow(dead_code)]
mod util;
//! # [Ratatui] Layout example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use crate::util::event::{Event, Events};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
layout::{Constraint, Direction, Layout},
widgets::{Block, Borders},
Terminal,
use itertools::Itertools;
use ratatui::{
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{
Constraint::{self, Length, Max, Min, Percentage, Ratio},
Layout, Rect,
},
style::{Color, Style, Stylize},
text::Line,
widgets::{Block, Paragraph},
DefaultTerminal, Frame,
};
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let events = Events::new();
fn main() -> color_eyre::Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = run(terminal);
ratatui::restore();
app_result
}
fn run(mut terminal: DefaultTerminal) -> color_eyre::Result<()> {
loop {
terminal.draw(|mut f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage(10),
Constraint::Percentage(80),
Constraint::Percentage(10),
]
.as_ref(),
)
.split(f.size());
let block = Block::default().title("Block").borders(Borders::ALL);
f.render_widget(block, chunks[0]);
let block = Block::default().title("Block 2").borders(Borders::ALL);
f.render_widget(block, chunks[2]);
})?;
match events.next()? {
Event::Input(input) => {
if let Key::Char('q') = input {
break;
}
terminal.draw(draw)?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
break Ok(());
}
_ => {}
}
}
Ok(())
}
#[allow(clippy::too_many_lines)]
fn draw(frame: &mut Frame) {
let vertical = Layout::vertical([
Length(4), // text
Length(50), // examples
Min(0), // fills remaining space
]);
let [text_area, examples_area, _] = vertical.areas(frame.area());
// title
frame.render_widget(
Paragraph::new(vec![
Line::from("Horizontal Layout Example. Press q to quit".dark_gray()).centered(),
Line::from("Each line has 2 constraints, plus Min(0) to fill the remaining space."),
Line::from("E.g. the second line of the Len/Min box is [Length(2), Min(2), Min(0)]"),
Line::from("Note: constraint labels that don't fit are truncated"),
]),
text_area,
);
let example_rows = Layout::vertical([
Length(9),
Length(9),
Length(9),
Length(9),
Length(9),
Min(0), // fills remaining space
])
.split(examples_area);
let example_areas = example_rows.iter().flat_map(|area| {
Layout::horizontal([
Length(14),
Length(14),
Length(14),
Length(14),
Length(14),
Min(0), // fills remaining space
])
.split(*area)
.iter()
.copied()
.take(5) // ignore Min(0)
.collect_vec()
});
// the examples are a cartesian product of the following constraints
// e.g. Len/Len, Len/Min, Len/Max, Len/Perc, Len/Ratio, Min/Len, Min/Min, ...
let examples = [
(
"Len",
[
Length(0),
Length(2),
Length(3),
Length(6),
Length(10),
Length(15),
],
),
("Min", [Min(0), Min(2), Min(3), Min(6), Min(10), Min(15)]),
("Max", [Max(0), Max(2), Max(3), Max(6), Max(10), Max(15)]),
(
"Perc",
[
Percentage(0),
Percentage(25),
Percentage(50),
Percentage(75),
Percentage(100),
Percentage(150),
],
),
(
"Ratio",
[
Ratio(0, 4),
Ratio(1, 4),
Ratio(2, 4),
Ratio(3, 4),
Ratio(4, 4),
Ratio(6, 4),
],
),
];
for ((a, b), area) in examples
.iter()
.cartesian_product(examples.iter())
.zip(example_areas)
{
let (name_a, examples_a) = a;
let (name_b, examples_b) = b;
let constraints = examples_a.iter().copied().zip(examples_b.iter().copied());
render_example_combination(frame, area, &format!("{name_a}/{name_b}"), constraints);
}
}
/// Renders a single example box
fn render_example_combination(
frame: &mut Frame,
area: Rect,
title: &str,
constraints: impl ExactSizeIterator<Item = (Constraint, Constraint)>,
) {
let block = Block::bordered()
.title(title.gray())
.style(Style::reset())
.border_style(Style::default().fg(Color::DarkGray));
let inner = block.inner(area);
frame.render_widget(block, area);
let layout = Layout::vertical(vec![Length(1); constraints.len() + 1]).split(inner);
for ((a, b), &area) in constraints.into_iter().zip(layout.iter()) {
render_single_example(frame, area, vec![a, b, Min(0)]);
}
// This is to make it easy to visually see the alignment of the examples
// with the constraints.
frame.render_widget(Paragraph::new("123456789012"), layout[6]);
}
/// Renders a single example line
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();
let horizontal = Layout::horizontal(constraints);
let [r, b, g] = horizontal.areas(area);
frame.render_widget(red, r);
frame.render_widget(blue, b);
frame.render_widget(green, g);
}
fn constraint_label(constraint: Constraint) -> String {
match constraint {
Constraint::Ratio(a, b) => format!("{a}:{b}"),
Constraint::Length(n)
| Constraint::Min(n)
| Constraint::Max(n)
| Constraint::Percentage(n)
| Constraint::Fill(n) => format!("{n}"),
}
}

179
examples/line_gauge.rs Normal file
View File

@@ -0,0 +1,179 @@
//! # [Ratatui] Line Gauge example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use std::time::Duration;
use color_eyre::Result;
use ratatui::{
buffer::Buffer,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Alignment, Constraint, Layout, Rect},
style::{palette::tailwind, Color, Style, Stylize},
text::Line,
widgets::{Block, Borders, LineGauge, Padding, Paragraph, Widget},
DefaultTerminal,
};
const CUSTOM_LABEL_COLOR: Color = tailwind::SLATE.c200;
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::default().run(terminal);
ratatui::restore();
app_result
}
#[derive(Debug, Default, Clone, Copy)]
struct App {
state: AppState,
progress_columns: u16,
progress: f64,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum AppState {
#[default]
Running,
Started,
Quitting,
}
impl App {
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
while self.state != AppState::Quitting {
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
self.handle_events()?;
self.update(terminal.size()?.width);
}
Ok(())
}
fn update(&mut self, terminal_width: u16) {
if self.state != AppState::Started {
return;
}
self.progress_columns = (self.progress_columns + 1).clamp(0, terminal_width);
self.progress = f64::from(self.progress_columns) / f64::from(terminal_width);
}
fn handle_events(&mut self) -> Result<()> {
let timeout = Duration::from_secs_f32(1.0 / 20.0);
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char(' ') | KeyCode::Enter => self.start(),
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
_ => {}
}
}
}
}
Ok(())
}
fn start(&mut self) {
self.state = AppState::Started;
}
fn quit(&mut self) {
self.state = AppState::Quitting;
}
}
impl Widget for &App {
fn render(self, area: Rect, buf: &mut Buffer) {
use Constraint::{Length, Min, Ratio};
let layout = Layout::vertical([Length(2), Min(0), Length(1)]);
let [header_area, main_area, footer_area] = layout.areas(area);
let layout = Layout::vertical([Ratio(1, 3); 3]);
let [gauge1_area, gauge2_area, gauge3_area] = layout.areas(main_area);
header().render(header_area, buf);
footer().render(footer_area, buf);
self.render_gauge1(gauge1_area, buf);
self.render_gauge2(gauge2_area, buf);
self.render_gauge3(gauge3_area, buf);
}
}
fn header() -> impl Widget {
Paragraph::new("Ratatui Line Gauge Example")
.bold()
.alignment(Alignment::Center)
.fg(CUSTOM_LABEL_COLOR)
}
fn footer() -> impl Widget {
Paragraph::new("Press ENTER / SPACE to start")
.alignment(Alignment::Center)
.fg(CUSTOM_LABEL_COLOR)
.bold()
}
impl App {
fn render_gauge1(&self, area: Rect, buf: &mut Buffer) {
let title = title_block("Blue / red only foreground");
LineGauge::default()
.block(title)
.filled_style(Style::default().fg(Color::Blue))
.unfilled_style(Style::default().fg(Color::Red))
.label("Foreground:")
.ratio(self.progress)
.render(area, buf);
}
fn render_gauge2(&self, area: Rect, buf: &mut Buffer) {
let title = title_block("Blue / red only background");
LineGauge::default()
.block(title)
.filled_style(Style::default().fg(Color::Blue).bg(Color::Blue))
.unfilled_style(Style::default().fg(Color::Red).bg(Color::Red))
.label("Background:")
.ratio(self.progress)
.render(area, buf);
}
fn render_gauge3(&self, area: Rect, buf: &mut Buffer) {
let title = title_block("Fully styled with background");
LineGauge::default()
.block(title)
.filled_style(
Style::default()
.fg(tailwind::BLUE.c400)
.bg(tailwind::BLUE.c600),
)
.unfilled_style(
Style::default()
.fg(tailwind::RED.c400)
.bg(tailwind::RED.c800),
)
.label("Both:")
.ratio(self.progress)
.render(area, buf);
}
}
fn title_block(title: &str) -> Block {
Block::default()
.title(Line::from(title).centered())
.borders(Borders::NONE)
.fg(CUSTOM_LABEL_COLOR)
.padding(Padding::vertical(1))
}

View File

@@ -1,147 +1,287 @@
#[allow(dead_code)]
mod util;
//! # [Ratatui] List example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use crate::util::{
event::{Event, Events},
StatefulList,
};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
layout::{Constraint, Corner, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, List, Text},
Terminal,
use color_eyre::Result;
use ratatui::{
buffer::Buffer,
crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind},
layout::{Constraint, Layout, Rect},
style::{
palette::tailwind::{BLUE, GREEN, SLATE},
Color, Modifier, Style, Stylize,
},
symbols,
text::Line,
widgets::{
Block, Borders, HighlightSpacing, List, ListItem, ListState, Padding, Paragraph,
StatefulWidget, Widget, Wrap,
},
DefaultTerminal,
};
struct App<'a> {
items: StatefulList<&'a str>,
events: Vec<(&'a str, &'a str)>,
info_style: Style,
warning_style: Style,
error_style: Style,
critical_style: Style,
const TODO_HEADER_STYLE: Style = Style::new().fg(SLATE.c100).bg(BLUE.c800);
const NORMAL_ROW_BG: Color = SLATE.c950;
const ALT_ROW_BG_COLOR: Color = SLATE.c900;
const SELECTED_STYLE: Style = Style::new().bg(SLATE.c800).add_modifier(Modifier::BOLD);
const TEXT_FG_COLOR: Color = SLATE.c200;
const COMPLETED_TEXT_FG_COLOR: Color = GREEN.c500;
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::default().run(terminal);
ratatui::restore();
app_result
}
impl<'a> App<'a> {
fn new() -> App<'a> {
App {
items: StatefulList::with_items(vec![
"Item0", "Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8",
"Item9", "Item10", "Item11", "Item12", "Item13", "Item14", "Item15", "Item16",
"Item17", "Item18", "Item19", "Item20", "Item21", "Item22", "Item23", "Item24",
/// This struct holds the current state of the app. In particular, it has the `todo_list` field
/// which is a wrapper around `ListState`. Keeping track of the state lets us render the
/// associated widget with its state and have access to features such as natural scrolling.
///
/// Check the event handling at the bottom to see how to change the state on incoming events. Check
/// the drawing logic for items on how to specify the highlighting style for selected items.
struct App {
should_exit: bool,
todo_list: TodoList,
}
struct TodoList {
items: Vec<TodoItem>,
state: ListState,
}
#[derive(Debug)]
struct TodoItem {
todo: String,
info: String,
status: Status,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Status {
Todo,
Completed,
}
impl Default for App {
fn default() -> Self {
Self {
should_exit: false,
todo_list: TodoList::from_iter([
(Status::Todo, "Rewrite everything with Rust!", "I can't hold my inner voice. He tells me to rewrite the complete universe with Rust"),
(Status::Completed, "Rewrite all of your tui apps with Ratatui", "Yes, you heard that right. Go and replace your tui with Ratatui."),
(Status::Todo, "Pet your cat", "Minnak loves to be pet by you! Don't forget to pet and give some treats!"),
(Status::Todo, "Walk with your dog", "Max is bored, go walk with him!"),
(Status::Completed, "Pay the bills", "Pay the train subscription!!!"),
(Status::Completed, "Refactor list example", "If you see this info that means I completed this task!"),
]),
events: vec![
("Event1", "INFO"),
("Event2", "INFO"),
("Event3", "CRITICAL"),
("Event4", "ERROR"),
("Event5", "INFO"),
("Event6", "INFO"),
("Event7", "WARNING"),
("Event8", "INFO"),
("Event9", "INFO"),
("Event10", "INFO"),
("Event11", "CRITICAL"),
("Event12", "INFO"),
("Event13", "INFO"),
("Event14", "INFO"),
("Event15", "INFO"),
("Event16", "INFO"),
("Event17", "ERROR"),
("Event18", "ERROR"),
("Event19", "INFO"),
("Event20", "INFO"),
("Event21", "WARNING"),
("Event22", "INFO"),
("Event23", "INFO"),
("Event24", "WARNING"),
("Event25", "INFO"),
("Event26", "INFO"),
],
info_style: Style::default().fg(Color::White),
warning_style: Style::default().fg(Color::Yellow),
error_style: Style::default().fg(Color::Magenta),
critical_style: Style::default().fg(Color::Red),
}
}
}
impl FromIterator<(Status, &'static str, &'static str)> for TodoList {
fn from_iter<I: IntoIterator<Item = (Status, &'static str, &'static str)>>(iter: I) -> Self {
let items = iter
.into_iter()
.map(|(status, todo, info)| TodoItem::new(status, todo, info))
.collect();
let state = ListState::default();
Self { items, state }
}
}
impl TodoItem {
fn new(status: Status, todo: &str, info: &str) -> Self {
Self {
status,
todo: todo.to_string(),
info: info.to_string(),
}
}
}
impl App {
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
while !self.should_exit {
terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?;
if let Event::Key(key) = event::read()? {
self.handle_key(key);
};
}
Ok(())
}
fn handle_key(&mut self, key: KeyEvent) {
if key.kind != KeyEventKind::Press {
return;
}
match key.code {
KeyCode::Char('q') | KeyCode::Esc => self.should_exit = true,
KeyCode::Char('h') | KeyCode::Left => self.select_none(),
KeyCode::Char('j') | KeyCode::Down => self.select_next(),
KeyCode::Char('k') | KeyCode::Up => self.select_previous(),
KeyCode::Char('g') | KeyCode::Home => self.select_first(),
KeyCode::Char('G') | KeyCode::End => self.select_last(),
KeyCode::Char('l') | KeyCode::Right | KeyCode::Enter => {
self.toggle_status();
}
_ => {}
}
}
fn advance(&mut self) {
let event = self.events.pop().unwrap();
self.events.insert(0, event);
fn select_none(&mut self) {
self.todo_list.state.select(None);
}
}
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
fn select_next(&mut self) {
self.todo_list.state.select_next();
}
fn select_previous(&mut self) {
self.todo_list.state.select_previous();
}
let events = Events::new();
fn select_first(&mut self) {
self.todo_list.state.select_first();
}
// App
let mut app = App::new();
fn select_last(&mut self) {
self.todo_list.state.select_last();
}
loop {
terminal.draw(|mut f| {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
let style = Style::default().fg(Color::Black).bg(Color::White);
let items = app.items.items.iter().map(|i| Text::raw(*i));
let items = List::new(items)
.block(Block::default().borders(Borders::ALL).title("List"))
.style(style)
.highlight_style(style.fg(Color::LightGreen).modifier(Modifier::BOLD))
.highlight_symbol(">");
f.render_stateful_widget(items, chunks[0], &mut app.items.state);
let events = app.events.iter().map(|&(evt, level)| {
Text::styled(
format!("{}: {}", level, evt),
match level {
"ERROR" => app.error_style,
"CRITICAL" => app.critical_style,
"WARNING" => app.warning_style,
_ => app.info_style,
},
)
});
let events_list = List::new(events)
.block(Block::default().borders(Borders::ALL).title("List"))
.start_corner(Corner::BottomLeft);
f.render_widget(events_list, chunks[1]);
})?;
match events.next()? {
Event::Input(input) => match input {
Key::Char('q') => {
break;
}
Key::Left => {
app.items.unselect();
}
Key::Down => {
app.items.next();
}
Key::Up => {
app.items.previous();
}
_ => {}
},
Event::Tick => {
app.advance();
/// Changes the status of the selected list item
fn toggle_status(&mut self) {
if let Some(i) = self.todo_list.state.selected() {
self.todo_list.items[i].status = match self.todo_list.items[i].status {
Status::Completed => Status::Todo,
Status::Todo => Status::Completed,
}
}
}
Ok(())
}
impl Widget for &mut App {
fn render(self, area: Rect, buf: &mut Buffer) {
let [header_area, main_area, footer_area] = Layout::vertical([
Constraint::Length(2),
Constraint::Fill(1),
Constraint::Length(1),
])
.areas(area);
let [list_area, item_area] =
Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)]).areas(main_area);
App::render_header(header_area, buf);
App::render_footer(footer_area, buf);
self.render_list(list_area, buf);
self.render_selected_item(item_area, buf);
}
}
/// Rendering logic for the app
impl App {
fn render_header(area: Rect, buf: &mut Buffer) {
Paragraph::new("Ratatui List Example")
.bold()
.centered()
.render(area, buf);
}
fn render_footer(area: Rect, buf: &mut Buffer) {
Paragraph::new("Use ↓↑ to move, ← to unselect, → to change status, g/G to go top/bottom.")
.centered()
.render(area, buf);
}
fn render_list(&mut self, area: Rect, buf: &mut Buffer) {
let block = Block::new()
.title(Line::raw("TODO List").centered())
.borders(Borders::TOP)
.border_set(symbols::border::EMPTY)
.border_style(TODO_HEADER_STYLE)
.bg(NORMAL_ROW_BG);
// Iterate through all elements in the `items` and stylize them.
let items: Vec<ListItem> = self
.todo_list
.items
.iter()
.enumerate()
.map(|(i, todo_item)| {
let color = alternate_colors(i);
ListItem::from(todo_item).bg(color)
})
.collect();
// Create a List from all list items and highlight the currently selected one
let list = List::new(items)
.block(block)
.highlight_style(SELECTED_STYLE)
.highlight_symbol(">")
.highlight_spacing(HighlightSpacing::Always);
// We need to disambiguate this trait method as both `Widget` and `StatefulWidget` share the
// same method name `render`.
StatefulWidget::render(list, area, buf, &mut self.todo_list.state);
}
fn render_selected_item(&self, area: Rect, buf: &mut Buffer) {
// We get the info depending on the item's state.
let info = if let Some(i) = self.todo_list.state.selected() {
match self.todo_list.items[i].status {
Status::Completed => format!("✓ DONE: {}", self.todo_list.items[i].info),
Status::Todo => format!("☐ TODO: {}", self.todo_list.items[i].info),
}
} else {
"Nothing selected...".to_string()
};
// We show the list item's info under the list in this paragraph
let block = Block::new()
.title(Line::raw("TODO Info").centered())
.borders(Borders::TOP)
.border_set(symbols::border::EMPTY)
.border_style(TODO_HEADER_STYLE)
.bg(NORMAL_ROW_BG)
.padding(Padding::horizontal(1));
// We can now render the item info
Paragraph::new(info)
.block(block)
.fg(TEXT_FG_COLOR)
.wrap(Wrap { trim: false })
.render(area, buf);
}
}
const fn alternate_colors(i: usize) -> Color {
if i % 2 == 0 {
NORMAL_ROW_BG
} else {
ALT_ROW_BG_COLOR
}
}
impl From<&TodoItem> for ListItem<'_> {
fn from(value: &TodoItem) -> Self {
let line = match value.status {
Status::Todo => Line::styled(format!("{}", value.todo), TEXT_FG_COLOR),
Status::Completed => {
Line::styled(format!("{}", value.todo), COMPLETED_TEXT_FG_COLOR)
}
};
ListItem::new(line)
}
}

40
examples/minimal.rs Normal file
View File

@@ -0,0 +1,40 @@
//! # [Ratatui] Minimal example
//!
//! This is a bare minimum example. There are many approaches to running an application loop, so
//! this is not meant to be prescriptive. See the [examples] folder for more complete examples.
//! In particular, the [hello-world] example is a good starting point.
//!
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [hello-world]: https://github.com/ratatui-org/ratatui/blob/main/examples/hello_world.rs
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use ratatui::{
crossterm::event::{self, Event},
text::Text,
Frame,
};
fn main() {
let mut terminal = ratatui::init();
loop {
terminal
.draw(|frame: &mut Frame| frame.render_widget(Text::raw("Hello World!"), frame.area()))
.expect("failed to draw frame");
if matches!(event::read().expect("failed to read event"), Event::Key(_)) {
break;
}
}
ratatui::restore();
}

100
examples/modifiers.rs Normal file
View File

@@ -0,0 +1,100 @@
//! # [Ratatui] Modifiers example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
// This example is useful for testing how your terminal emulator handles different modifiers.
// It will render a grid of combinations of foreground and background colors with all
// modifiers applied to them.
use std::{error::Error, iter::once, result};
use itertools::Itertools;
use ratatui::{
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Layout},
style::{Color, Modifier, Style, Stylize},
text::Line,
widgets::Paragraph,
DefaultTerminal, Frame,
};
type Result<T> = result::Result<T, Box<dyn Error>>;
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = run(terminal);
ratatui::restore();
app_result
}
fn run(mut terminal: DefaultTerminal) -> Result<()> {
loop {
terminal.draw(draw)?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(());
}
}
}
}
fn draw(frame: &mut Frame) {
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
let [text_area, main_area] = vertical.areas(frame.area());
frame.render_widget(
Paragraph::new("Note: not all terminals support all modifiers")
.style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
text_area,
);
let layout = Layout::vertical([Constraint::Length(1); 50])
.split(main_area)
.iter()
.flat_map(|area| {
Layout::horizontal([Constraint::Percentage(20); 5])
.split(*area)
.to_vec()
})
.collect_vec();
let colors = [
Color::Black,
Color::DarkGray,
Color::Gray,
Color::White,
Color::Red,
];
let all_modifiers = once(Modifier::empty())
.chain(Modifier::all().iter())
.collect_vec();
let mut index = 0;
for bg in &colors {
for fg in &colors {
for modifier in &all_modifiers {
let modifier_name = format!("{modifier:11?}");
let padding = (" ").repeat(12 - modifier_name.len());
let paragraph = Paragraph::new(Line::from(vec![
modifier_name.fg(*fg).bg(*bg).add_modifier(*modifier),
padding.fg(*fg).bg(*bg).add_modifier(*modifier),
// This is a hack to work around a bug in VHS which is used for rendering the
// examples to gifs. The bug is that the background color of a paragraph seems
// to bleed into the next character.
".".black().on_black(),
]));
frame.render_widget(paragraph, layout[index]);
index += 1;
}
}
}
}

110
examples/panic.rs Normal file
View File

@@ -0,0 +1,110 @@
//! # [Ratatui] Panic Hook example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
//!
//! Prior to Ratatui 0.28.1, a panic hook had to be manually set up to ensure that the terminal was
//! reset when a panic occurred. This was necessary because a panic would interrupt the normal
//! control flow and leave the terminal in a distorted state.
//!
//! Starting with Ratatui 0.28.1, the panic hook is automatically set up by the new `ratatui::init`
//! function, so you no longer need to manually set up the panic hook. This example now demonstrates
//! how the panic hook acts when it is enabled by default.
//!
//! When exiting normally or when handling `Result::Err`, we can reset the terminal manually at the
//! end of `main` just before we print the error.
//!
//! Because a panic interrupts the normal control flow, manually resetting the terminal at the end
//! of `main` won't do us any good. Instead, we need to make sure to set up a panic hook that first
//! resets the terminal before handling the panic. This both reuses the standard panic hook to
//! ensure a consistent panic handling UX and properly resets the terminal to not distort the
//! output.
//!
//! That's why this example is set up to show both situations, with and without the panic hook, to
//! see the difference.
//!
//! For more information on how to set this up manually, see the [Color Eyre recipe] in the Ratatui
//! website.
//!
//! [Color Eyre recipe]: https://ratatui.rs/recipes/apps/color-eyre
use color_eyre::{eyre::bail, Result};
use ratatui::{
crossterm::event::{self, Event, KeyCode},
text::Line,
widgets::{Block, Paragraph},
DefaultTerminal, Frame,
};
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::new().run(terminal);
ratatui::restore();
app_result
}
struct App {
hook_enabled: bool,
}
impl App {
const fn new() -> Self {
Self { hook_enabled: true }
}
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
loop {
terminal.draw(|frame| self.draw(frame))?;
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('p') => panic!("intentional demo panic"),
KeyCode::Char('e') => bail!("intentional demo error"),
KeyCode::Char('h') => {
let _ = std::panic::take_hook();
self.hook_enabled = false;
}
KeyCode::Char('q') => return Ok(()),
_ => {}
}
}
}
}
fn draw(&self, frame: &mut Frame) {
let text = vec![
if self.hook_enabled {
Line::from("HOOK IS CURRENTLY **ENABLED**")
} else {
Line::from("HOOK IS CURRENTLY **DISABLED**")
},
Line::from(""),
Line::from("Press `p` to cause a panic"),
Line::from("Press `e` to cause an error"),
Line::from("Press `h` to disable the panic hook"),
Line::from("Press `q` to quit"),
Line::from(""),
Line::from("When your app panics without a panic hook, you will likely have to"),
Line::from("reset your terminal afterwards with the `reset` command"),
Line::from(""),
Line::from("Try first with the panic handler enabled, and then with it disabled"),
Line::from("to see the difference"),
];
let paragraph = Paragraph::new(text)
.block(Block::bordered().title("Panic Handler Demo"))
.centered();
frame.render_widget(paragraph, frame.area());
}
}

View File

@@ -1,107 +1,153 @@
#[allow(dead_code)]
mod util;
//! # [Ratatui] Paragraph example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use crate::util::event::{Event, Events};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Paragraph, Text},
Terminal,
use std::{
io::{self},
time::{Duration, Instant},
};
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
use color_eyre::Result;
use ratatui::{
buffer::Buffer,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Layout, Rect},
style::{Color, Stylize},
text::{Line, Masked, Span},
widgets::{Block, Paragraph, Widget, Wrap},
DefaultTerminal,
};
let events = Events::new();
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::new().run(terminal);
ratatui::restore();
app_result
}
let mut scroll: u16 = 0;
loop {
terminal.draw(|mut f| {
let size = f.size();
#[derive(Debug)]
struct App {
should_exit: bool,
scroll: u16,
last_tick: Instant,
}
// Words made "loooong" to demonstrate line breaking.
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
long_line.push('\n');
impl App {
/// The duration between each tick.
const TICK_RATE: Duration = Duration::from_millis(250);
let block = Block::default()
.style(Style::default().bg(Color::White));
f.render_widget(block, size);
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(5)
.constraints(
[
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
]
.as_ref(),
)
.split(size);
let text = [
Text::raw("This is a line \n"),
Text::styled("This is a line \n", Style::default().fg(Color::Red)),
Text::styled("This is a line\n", Style::default().bg(Color::Blue)),
Text::styled(
"This is a longer line\n",
Style::default().modifier(Modifier::CROSSED_OUT),
),
Text::styled(&long_line, Style::default().bg(Color::Green)),
Text::styled(
"This is a line\n",
Style::default().fg(Color::Green).modifier(Modifier::ITALIC),
),
];
let block = Block::default()
.borders(Borders::ALL)
.title_style(Style::default().modifier(Modifier::BOLD));
let paragraph = Paragraph::new(text.iter())
.block(block.clone().title("Left, no wrap"))
.alignment(Alignment::Left);
f.render_widget(paragraph, chunks[0]);
let paragraph = Paragraph::new(text.iter())
.block(block.clone().title("Left, wrap"))
.alignment(Alignment::Left)
.wrap(true);
f.render_widget(paragraph, chunks[1]);
let paragraph = Paragraph::new(text.iter())
.block(block.clone().title("Center, wrap"))
.alignment(Alignment::Center)
.wrap(true)
.scroll(scroll);
f.render_widget(paragraph, chunks[2]);
let paragraph = Paragraph::new(text.iter())
.block(block.clone().title("Right, wrap"))
.alignment(Alignment::Right)
.wrap(true);
f.render_widget(paragraph, chunks[3]);
})?;
scroll += 1;
scroll %= 10;
match events.next()? {
Event::Input(key) => {
if key == Key::Char('q') {
break;
}
}
_ => {}
/// Create a new instance of the app.
fn new() -> Self {
Self {
should_exit: false,
scroll: 0,
last_tick: Instant::now(),
}
}
Ok(())
/// Run the app until the user exits.
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
while !self.should_exit {
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
self.handle_events()?;
if self.last_tick.elapsed() >= Self::TICK_RATE {
self.on_tick();
self.last_tick = Instant::now();
}
}
Ok(())
}
/// Handle events from the terminal.
fn handle_events(&mut self) -> io::Result<()> {
let timeout = Self::TICK_RATE.saturating_sub(self.last_tick.elapsed());
while event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
self.should_exit = true;
}
}
}
Ok(())
}
/// Update the app state on each tick.
fn on_tick(&mut self) {
self.scroll = (self.scroll + 1) % 10;
}
}
impl Widget for &App {
fn render(self, area: Rect, buf: &mut Buffer) {
let areas = Layout::vertical([Constraint::Max(9); 4]).split(area);
Paragraph::new(create_lines(area))
.block(title_block("Default alignment (Left), no wrap"))
.gray()
.render(areas[0], buf);
Paragraph::new(create_lines(area))
.block(title_block("Default alignment (Left), with wrap"))
.gray()
.wrap(Wrap { trim: true })
.render(areas[1], buf);
Paragraph::new(create_lines(area))
.block(title_block("Right alignment, with wrap"))
.gray()
.right_aligned()
.wrap(Wrap { trim: true })
.render(areas[2], buf);
Paragraph::new(create_lines(area))
.block(title_block("Center alignment, with wrap, with scroll"))
.gray()
.centered()
.wrap(Wrap { trim: true })
.scroll((self.scroll, 0))
.render(areas[3], buf);
}
}
/// Create a bordered block with a title.
fn title_block(title: &str) -> Block {
Block::bordered()
.gray()
.title(title.bold().into_centered_line())
}
/// Create some lines to display in the paragraph.
fn create_lines(area: Rect) -> Vec<Line<'static>> {
let short_line = "A long line to demonstrate line wrapping. ";
let long_line = short_line.repeat(usize::from(area.width) / short_line.len() + 4);
let mut styled_spans = vec![];
for span in [
"Styled".blue(),
"Spans".red().on_white(),
"Bold".bold(),
"Italic".italic(),
"Underlined".underlined(),
"Strikethrough".crossed_out(),
] {
styled_spans.push(span);
styled_spans.push(" ".into());
}
vec![
Line::raw("Unstyled Line"),
Line::raw("Styled Line").black().on_red().bold().italic(),
Line::from(styled_spans),
Line::from(long_line.green().italic()),
Line::from_iter([
"Masked text: ".into(),
Span::styled(Masked::new("my secret password", '*'), Color::Red),
]),
]
}

View File

@@ -1,111 +1,93 @@
#[allow(dead_code)]
mod util;
//! # [Ratatui] Popup example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use crate::util::event::{Event, Events};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::layout::Rect;
use tui::widgets::Clear;
use tui::{
backend::TermionBackend,
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Paragraph, Text},
Terminal,
// See also https://github.com/joshka/tui-popup and
// https://github.com/sephiroth74/tui-confirm-dialog
use color_eyre::Result;
use ratatui::{
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Flex, Layout, Rect},
style::Stylize,
widgets::{Block, Clear, Paragraph, Wrap},
DefaultTerminal, Frame,
};
/// helper function to create a centered rect using up
/// certain percentage of the available rect `r`
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
]
.as_ref(),
)
.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(),
)
.split(popup_layout[1])[1]
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::default().run(terminal);
ratatui::restore();
app_result
}
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
#[derive(Default)]
struct App {
show_popup: bool,
}
let events = Events::new();
impl App {
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
loop {
terminal.draw(|frame| self.draw(frame))?;
loop {
terminal.draw(|mut f| {
let size = f.size();
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(size);
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
let mut long_line = s.repeat(usize::from(size.width)*usize::from(size.height)/300);
long_line.push('\n');
let text = [
Text::raw("This is a line \n"),
Text::styled("This is a line \n", Style::default().fg(Color::Red)),
Text::styled("This is a line\n", Style::default().bg(Color::Blue)),
Text::styled(
"This is a longer line\n",
Style::default().modifier(Modifier::CROSSED_OUT),
),
Text::styled(&long_line, Style::default().bg(Color::Green)),
Text::styled(
"This is a line\n",
Style::default().fg(Color::Green).modifier(Modifier::ITALIC),
),
];
let paragraph = Paragraph::new(text.iter())
.block(Block::default().title("Left Block").borders(Borders::ALL))
.alignment(Alignment::Left).wrap(true);
f.render_widget(paragraph, chunks[0]);
let paragraph = Paragraph::new(text.iter())
.block(Block::default().title("Right Block").borders(Borders::ALL))
.alignment(Alignment::Left).wrap(true);
f.render_widget(paragraph, chunks[1]);
let block = Block::default().title("Popup").borders(Borders::ALL);
let area = centered_rect(60, 20, size);
f.render_widget(Clear, area); //this clears out the background
f.render_widget(block, area);
})?;
match events.next()? {
Event::Input(input) => {
if let Key::Char('q') = input {
break;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Char('p') => self.show_popup = !self.show_popup,
_ => {}
}
}
}
_ => {}
}
}
Ok(())
fn draw(&self, frame: &mut Frame) {
let area = frame.area();
let vertical = Layout::vertical([Constraint::Percentage(20), Constraint::Percentage(80)]);
let [instructions, content] = vertical.areas(area);
let text = if self.show_popup {
"Press p to close the popup"
} else {
"Press p to show the popup"
};
let paragraph = Paragraph::new(text.slow_blink())
.centered()
.wrap(Wrap { trim: true });
frame.render_widget(paragraph, instructions);
let block = Block::bordered().title("Content").on_blue();
frame.render_widget(block, content);
if self.show_popup {
let block = Block::bordered().title("Popup");
let area = popup_area(area, 60, 20);
frame.render_widget(Clear, area); //this clears out the background
frame.render_widget(block, area);
}
}
}
/// helper function to create a centered rect using up certain percentage of the available rect `r`
fn popup_area(area: Rect, percent_x: u16, percent_y: u16) -> Rect {
let vertical = Layout::vertical([Constraint::Percentage(percent_y)]).flex(Flex::Center);
let horizontal = Layout::horizontal([Constraint::Percentage(percent_x)]).flex(Flex::Center);
let [area] = vertical.areas(area);
let [area] = horizontal.areas(area);
area
}

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

@@ -0,0 +1,69 @@
//! # [Ratatui] Logo example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use std::{
io::{self},
thread::sleep,
time::Duration,
};
use indoc::indoc;
use itertools::izip;
use ratatui::{widgets::Paragraph, TerminalOptions, Viewport};
/// A fun example of using half block characters to draw a logo
#[allow(clippy::many_single_char_names)]
fn logo() -> String {
let r = indoc! {"
▄▄▄
█▄▄▀
█ █
"};
let a = indoc! {"
▄▄
█▄▄█
█ █
"};
let t = indoc! {"
▄▄▄
"};
let u = indoc! {"
▄ ▄
█ █
▀▄▄▀
"};
let i = indoc! {"
"};
izip!(r.lines(), a.lines(), t.lines(), u.lines(), i.lines())
.map(|(r, a, t, u, i)| format!("{r:5}{a:5}{t:4}{a:5}{t:4}{u:5}{i:5}"))
.collect::<Vec<_>>()
.join("\n")
}
fn main() -> io::Result<()> {
let mut terminal = ratatui::init_with_options(TerminalOptions {
viewport: Viewport::Inline(3),
});
terminal.draw(|frame| frame.render_widget(Paragraph::new(logo()), frame.area()))?;
sleep(Duration::from_secs(5));
ratatui::restore();
println!();
Ok(())
}

View File

@@ -1,68 +0,0 @@
mod demo;
#[allow(dead_code)]
mod util;
use crate::demo::{ui, App};
use argh::FromArgs;
use rustbox::keyboard::Key;
use std::{
error::Error,
time::{Duration, Instant},
};
use tui::{backend::RustboxBackend, Terminal};
/// Rustbox demo
#[derive(Debug, FromArgs)]
struct Cli {
/// time in ms between two ticks.
#[argh(option, default = "250")]
tick_rate: u64,
/// whether unicode symbols are used to improve the overall look of the app
#[argh(option, default = "true")]
enhanced_graphics: bool,
}
fn main() -> Result<(), Box<dyn Error>> {
let cli: Cli = argh::from_env();
let backend = RustboxBackend::new()?;
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
let mut app = App::new("Rustbox demo", cli.enhanced_graphics);
let mut last_tick = Instant::now();
let tick_rate = Duration::from_millis(cli.tick_rate);
loop {
terminal.draw(|mut f| ui::draw(&mut f, &mut app))?;
match terminal.backend().rustbox().peek_event(tick_rate, false) {
Ok(rustbox::Event::KeyEvent(key)) => match key {
Key::Char(c) => {
app.on_key(c);
}
Key::Up => {
app.on_up();
}
Key::Down => {
app.on_down();
}
Key::Left => {
app.on_left();
}
Key::Right => {
app.on_right();
}
_ => {}
},
_ => {}
}
if last_tick.elapsed() > tick_rate {
app.on_tick();
last_tick = Instant::now();
}
if app.should_quit {
break;
}
}
Ok(())
}

211
examples/scrollbar.rs Normal file
View File

@@ -0,0 +1,211 @@
//! # [Ratatui] Scrollbar example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
#![warn(clippy::pedantic)]
use std::time::{Duration, Instant};
use color_eyre::Result;
use ratatui::{
crossterm::event::{self, Event, KeyCode},
layout::{Alignment, Constraint, Layout, Margin},
style::{Color, Style, Stylize},
symbols::scrollbar,
text::{Line, Masked, Span},
widgets::{Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
DefaultTerminal, Frame,
};
#[derive(Default)]
struct App {
pub vertical_scroll_state: ScrollbarState,
pub horizontal_scroll_state: ScrollbarState,
pub vertical_scroll: usize,
pub horizontal_scroll: usize,
}
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::default().run(terminal);
ratatui::restore();
app_result
}
impl App {
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
let tick_rate = Duration::from_millis(250);
let mut last_tick = Instant::now();
loop {
terminal.draw(|frame| self.draw(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') => return Ok(()),
KeyCode::Char('j') | KeyCode::Down => {
self.vertical_scroll = self.vertical_scroll.saturating_add(1);
self.vertical_scroll_state =
self.vertical_scroll_state.position(self.vertical_scroll);
}
KeyCode::Char('k') | KeyCode::Up => {
self.vertical_scroll = self.vertical_scroll.saturating_sub(1);
self.vertical_scroll_state =
self.vertical_scroll_state.position(self.vertical_scroll);
}
KeyCode::Char('h') | KeyCode::Left => {
self.horizontal_scroll = self.horizontal_scroll.saturating_sub(1);
self.horizontal_scroll_state = self
.horizontal_scroll_state
.position(self.horizontal_scroll);
}
KeyCode::Char('l') | KeyCode::Right => {
self.horizontal_scroll = self.horizontal_scroll.saturating_add(1);
self.horizontal_scroll_state = self
.horizontal_scroll_state
.position(self.horizontal_scroll);
}
_ => {}
}
}
}
if last_tick.elapsed() >= tick_rate {
last_tick = Instant::now();
}
}
}
#[allow(clippy::too_many_lines, clippy::cast_possible_truncation)]
fn draw(&mut self, frame: &mut Frame) {
let area = frame.area();
// Words made "loooong" to demonstrate line breaking.
let s =
"Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
let mut long_line = s.repeat(usize::from(area.width) / s.len() + 4);
long_line.push('\n');
let chunks = Layout::vertical([
Constraint::Min(1),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
])
.split(area);
let text = vec![
Line::from("This is a line "),
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.clone()),
Line::from("This is a line".reset()),
Line::from(vec![
Span::raw("Masked text: "),
Span::styled(Masked::new("password", '*'), Style::new().fg(Color::Red)),
]),
Line::from("This is a line "),
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.clone()),
Line::from("This is a line".reset()),
Line::from(vec![
Span::raw("Masked text: "),
Span::styled(Masked::new("password", '*'), Style::new().fg(Color::Red)),
]),
];
self.vertical_scroll_state = self.vertical_scroll_state.content_length(text.len());
self.horizontal_scroll_state = self.horizontal_scroll_state.content_length(long_line.len());
let create_block = |title: &'static str| Block::bordered().gray().title(title.bold());
let title = Block::new()
.title_alignment(Alignment::Center)
.title("Use h j k l or ◄ ▲ ▼ ► to scroll ".bold());
frame.render_widget(title, chunks[0]);
let paragraph = Paragraph::new(text.clone())
.gray()
.block(create_block("Vertical scrollbar with arrows"))
.scroll((self.vertical_scroll as u16, 0));
frame.render_widget(paragraph, chunks[1]);
frame.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some(""))
.end_symbol(Some("")),
chunks[1],
&mut self.vertical_scroll_state,
);
let paragraph = Paragraph::new(text.clone())
.gray()
.block(create_block(
"Vertical scrollbar without arrows, without track symbol and mirrored",
))
.scroll((self.vertical_scroll as u16, 0));
frame.render_widget(paragraph, chunks[2]);
frame.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalLeft)
.symbols(scrollbar::VERTICAL)
.begin_symbol(None)
.track_symbol(None)
.end_symbol(None),
chunks[2].inner(Margin {
vertical: 1,
horizontal: 0,
}),
&mut self.vertical_scroll_state,
);
let paragraph = Paragraph::new(text.clone())
.gray()
.block(create_block(
"Horizontal scrollbar with only begin arrow & custom thumb symbol",
))
.scroll((0, self.horizontal_scroll as u16));
frame.render_widget(paragraph, chunks[3]);
frame.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.thumb_symbol("🬋")
.end_symbol(None),
chunks[3].inner(Margin {
vertical: 0,
horizontal: 1,
}),
&mut self.horizontal_scroll_state,
);
let paragraph = Paragraph::new(text.clone())
.gray()
.block(create_block(
"Horizontal scrollbar without arrows & custom thumb and track symbol",
))
.scroll((0, self.horizontal_scroll as u16));
frame.render_widget(paragraph, chunks[4]);
frame.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.thumb_symbol("")
.track_symbol(Some("")),
chunks[4].inner(Margin {
vertical: 0,
horizontal: 1,
}),
&mut self.horizontal_scroll_state,
);
}
}

View File

@@ -1,20 +1,41 @@
#[allow(dead_code)]
mod util;
//! # [Ratatui] Sparkline example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use crate::util::{
event::{Event, Events},
RandomSignal,
use std::time::{Duration, Instant};
use color_eyre::Result;
use rand::{
distributions::{Distribution, Uniform},
rngs::ThreadRng,
};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::{
backend::TermionBackend,
layout::{Constraint, Direction, Layout},
use ratatui::{
crossterm::event::{self, Event, KeyCode},
layout::{Constraint, Layout},
style::{Color, Style},
widgets::{Block, Borders, Sparkline},
Terminal,
DefaultTerminal, Frame,
};
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::new().run(terminal);
ratatui::restore();
app_result
}
struct App {
signal: RandomSignal,
data1: Vec<u64>,
@@ -22,13 +43,35 @@ struct App {
data3: Vec<u64>,
}
#[derive(Clone)]
struct RandomSignal {
distribution: Uniform<u64>,
rng: ThreadRng,
}
impl RandomSignal {
fn new(lower: u64, upper: u64) -> Self {
Self {
distribution: Uniform::new(lower, upper),
rng: rand::thread_rng(),
}
}
}
impl Iterator for RandomSignal {
type Item = u64;
fn next(&mut self) -> Option<u64> {
Some(self.distribution.sample(&mut self.rng))
}
}
impl App {
fn new() -> App {
fn new() -> Self {
let mut signal = RandomSignal::new(0, 100);
let data1 = signal.by_ref().take(200).collect::<Vec<u64>>();
let data2 = signal.by_ref().take(200).collect::<Vec<u64>>();
let data3 = signal.by_ref().take(200).collect::<Vec<u64>>();
App {
Self {
signal,
data1,
data2,
@@ -36,7 +79,7 @@ impl App {
}
}
fn update(&mut self) {
fn on_tick(&mut self) {
let value = self.signal.next().unwrap();
self.data1.pop();
self.data1.insert(0, value);
@@ -47,79 +90,63 @@ impl App {
self.data3.pop();
self.data3.insert(0, value);
}
}
fn main() -> Result<(), Box<dyn Error>> {
// Terminal initialization
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
let tick_rate = Duration::from_millis(250);
// Setup event handlers
let events = Events::new();
let mut last_tick = Instant::now();
loop {
terminal.draw(|frame| self.draw(frame))?;
// Create default app state
let mut app = App::new();
loop {
terminal.draw(|mut f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(7),
Constraint::Min(0),
]
.as_ref(),
)
.split(f.size());
let sparkline = Sparkline::default()
.block(
Block::default()
.title("Data1")
.borders(Borders::LEFT | Borders::RIGHT),
)
.data(&app.data1)
.style(Style::default().fg(Color::Yellow));
f.render_widget(sparkline, chunks[0]);
let sparkline = Sparkline::default()
.block(
Block::default()
.title("Data2")
.borders(Borders::LEFT | Borders::RIGHT),
)
.data(&app.data2)
.style(Style::default().bg(Color::Green));
f.render_widget(sparkline, chunks[1]);
// Multiline
let sparkline = Sparkline::default()
.block(
Block::default()
.title("Data3")
.borders(Borders::LEFT | Borders::RIGHT),
)
.data(&app.data3)
.style(Style::default().fg(Color::Red));
f.render_widget(sparkline, chunks[2]);
})?;
match events.next()? {
Event::Input(input) => {
if input == Key::Char('q') {
break;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
return Ok(());
}
}
}
Event::Tick => {
app.update();
if last_tick.elapsed() >= tick_rate {
self.on_tick();
last_tick = Instant::now();
}
}
}
Ok(())
fn draw(&self, frame: &mut Frame) {
let chunks = Layout::vertical([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Min(0),
])
.split(frame.area());
let sparkline = Sparkline::default()
.block(
Block::new()
.borders(Borders::LEFT | Borders::RIGHT)
.title("Data1"),
)
.data(&self.data1)
.style(Style::default().fg(Color::Yellow));
frame.render_widget(sparkline, chunks[0]);
let sparkline = Sparkline::default()
.block(
Block::new()
.borders(Borders::LEFT | Borders::RIGHT)
.title("Data2"),
)
.data(&self.data2)
.style(Style::default().bg(Color::Green));
frame.render_widget(sparkline, chunks[1]);
// Multiline
let sparkline = Sparkline::default()
.block(
Block::new()
.borders(Borders::LEFT | Borders::RIGHT)
.title("Data3"),
)
.data(&self.data3)
.style(Style::default().fg(Color::Red));
frame.render_widget(sparkline, chunks[2]);
}
}

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