Compare commits

..

122 Commits

Author SHA1 Message Date
Josh McKinney
5431a70c0c Make ratatui dev-dependecy in ratatui-termwiz just a path ref 2024-11-27 14:04:02 -08:00
Josh McKinney
65666905e6 Fix missing underline-color feature mapping to core flag for -crossterm 2024-11-27 14:03:05 -08:00
Josh McKinney
9703083288 Make ratatui dev-dependecy in ratatui-crossterm just a path ref 2024-11-27 14:01:02 -08:00
Josh McKinney
c079cafe4d Make ratatui dev-dependecy in ratatui-widgets just a path ref 2024-11-27 13:59:04 -08:00
Josh McKinney
421adbe5dd Add sub crate versions to workspace manifest 2024-11-27 13:55:35 -08:00
Josh McKinney
35e790cb0d chore: prepare alpha modularization release 2024-11-27 13:31:06 -08:00
Orhun Parmaksız
2b7ec5cb7f chore(widgets): enable calendar widget as default (#1521)
We now expect that you disable the default features if you want less
dependencies
2024-11-27 11:06:09 +03:00
Orhun Parmaksız
d291042e69 docs(block): revise the block example (#1520)
- Moves the block example from `ratatui` to `ratatui-widgets`
- Simplifies the example (bordered, styled, custom borders)

see #1512
2024-11-26 22:38:40 +03:00
dependabot[bot]
5c1c97d5a2 chore(deps): bump cargo_metadata from 0.18.1 to 0.19.0 (#1517)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-11-25 19:52:03 -08:00
dependabot[bot]
f51d1ccc07 chore(deps): bump clap-verbosity-flag from 2.2.3 to 3.0.0 (#1516)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-11-25 19:51:09 -08:00
dependabot[bot]
d137456ca1 chore(deps): bump octocrab from 0.42.0 to 0.42.1 (#1515)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-11-25 19:50:46 -08:00
dependabot[bot]
881fe3eff1 chore(deps): bump rustls from 0.23.15 to 0.23.18 (#1518)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-25 19:23:03 -08:00
Josh McKinney
99ac005b06 docs(widgets): Add simple barchart example (#1511) 2024-11-24 01:32:54 -08:00
Josh McKinney
f132fa1715 refactor(table): small readability improvements (#1510) 2024-11-24 01:32:44 -08:00
Emirhan TALA
369b18eef2 feat(barchart): reduce barchart creation verbosity (#1453)
Adds constructor methods for BarChart, BarGroup, and Bar
2024-11-24 00:06:16 -08:00
Eric Lunderberg
2ce958e38c fix(table): Allow display of additional table row, if row height > 1 (#1452) 2024-11-23 18:40:22 -08:00
Orhun Parmaksız
217c57cd60 refactor: modularize backends (#1508)
Backend code is now moved to `ratatui-crossterm`, `ratatui-termion` and
`ratatui-termwiz`. This should be backwards compatible with existing code.

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-11-23 15:23:40 -08:00
Orhun Parmaksız
3ae6bf1d6f docs(contributing): use cargo-xtask for intructions (#1509)
- Updates `CONTRIBUTING.md` about the usage of `xtask`
- Removes `Makefile.toml`
2024-11-23 10:53:40 -08:00
Josh McKinney
ec30390446 fix(canvas): round coordinates to nearest grid cell (#1507)
Previously the canvas coordinates were rounded towards zero, which
causes the rendering to be off by one pixel in some cases. It also meant
that pixels at the extreme edges of the canvas can only be drawn if the
point was exactly on the edge of the canvas. This commit rounds the
coordinates to the nearest integer instead. This may change the output
for some apps using Canvas / Charts.
2024-11-22 14:00:55 -08:00
Emirhan TALA
56d5e05762 feat(bar)!: update label and text_value to accept Into<> (#1471)
BREAKING CHANGE: label and text_value now accept `Into<>` types, which
breaks type inference.

```diff
- Bar::default().label("foo".into());
+ Bar::default().label("foo");
```

```diff
- Bar::default().text_value("bar".into());
+ Bar::default().text_value("bar");
```
2024-11-21 19:12:24 -08:00
Emirhan TALA
b76ad3b02e feat(bar): impl Styled for Bar (#1476)
Related: https://github.com/ratatui/ratatui/issues/683
2024-11-21 17:30:08 -08:00
Ivan Smoliakov
afd1ce179b fix(canvas): Lines that start outside the visible grid are now drawn (#1501)
Previously lines with points that were outside the canvas bounds were
not drawn at all. Now they are clipped to the bounds of the canvas so
that the portion of the line within the canvas is draw.

To facilitate this, a new `Painter::bounds()` method which returns the
bounds of the canvas is added.

Fixes: https://github.com/ratatui/ratatui/issues/1489
2024-11-21 16:16:11 -08:00
Ho Kim
8f282473b2 docs(readme): correct examples links (#1484) 2024-11-20 06:59:01 -08:00
Josh McKinney
36e2d1bda1 fix: add feature(doc_cfg) when generating docs (#1506) 2024-11-20 04:17:13 -08:00
dependabot[bot]
9d5aba69e9 chore(deps): bump serde from 1.0.214 to 1.0.215 (#1495)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.214 to
1.0.215.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/serde-rs/serde/releases">serde's
releases</a>.</em></p>
<blockquote>
<h2>v1.0.215</h2>
<ul>
<li>Produce warning when multiple fields or variants have the same
deserialization name (<a
href="https://redirect.github.com/serde-rs/serde/issues/2855">#2855</a>,
<a
href="https://redirect.github.com/serde-rs/serde/issues/2856">#2856</a>,
<a
href="https://redirect.github.com/serde-rs/serde/issues/2857">#2857</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="8939af48fe"><code>8939af4</code></a>
Release 1.0.215</li>
<li><a
href="fa5d58cd00"><code>fa5d58c</code></a>
Use ui test syntax that does not interfere with rustfmt</li>
<li><a
href="1a3cf4b3c1"><code>1a3cf4b</code></a>
Update PR 2562 ui tests</li>
<li><a
href="7d96352e96"><code>7d96352</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/serde/issues/2857">#2857</a>
from dtolnay/collide</li>
<li><a
href="111ecc5d8c"><code>111ecc5</code></a>
Update ui tests for warning on colliding aliases</li>
<li><a
href="edd6fe954b"><code>edd6fe9</code></a>
Revert &quot;Add checks for conflicts for aliases&quot;</li>
<li><a
href="a20e9249c5"><code>a20e924</code></a>
Revert &quot;pacify clippy&quot;</li>
<li><a
href="b1353a99cd"><code>b1353a9</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/serde/issues/2856">#2856</a>
from dtolnay/dename</li>
<li><a
href="c59e876bb3"><code>c59e876</code></a>
Produce a separate warning for every colliding name</li>
<li><a
href="7f1e697c0d"><code>7f1e697</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/serde/issues/2855">#2855</a>
from dtolnay/namespan</li>
<li>Additional commits viewable in <a
href="https://github.com/serde-rs/serde/compare/v1.0.214...v1.0.215">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=serde&package-manager=cargo&previous-version=1.0.214&new-version=1.0.215)](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-11-19 20:56:43 -08:00
dependabot[bot]
1b0d6b473b chore(deps): bump clap from 4.5.20 to 4.5.21 (#1496)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.20 to 4.5.21.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/clap-rs/clap/releases">clap's
releases</a>.</em></p>
<blockquote>
<h2>v4.5.21</h2>
<h2>[4.5.21] - 2024-11-13</h2>
<h3>Fixes</h3>
<ul>
<li><em>(parser)</em> Ensure defaults are filled in on error with
<code>ignore_errors(true)</code></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/clap-rs/clap/blob/master/CHANGELOG.md">clap's
changelog</a>.</em></p>
<blockquote>
<h2>[4.5.21] - 2024-11-13</h2>
<h3>Fixes</h3>
<ul>
<li><em>(parser)</em> Ensure defaults are filled in on error with
<code>ignore_errors(true)</code></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="03d722625a"><code>03d7226</code></a>
chore: Release</li>
<li><a
href="3df70fb2b6"><code>3df70fb</code></a>
docs: Update changelog</li>
<li><a
href="3266c36abf"><code>3266c36</code></a>
Merge pull request <a
href="https://redirect.github.com/clap-rs/clap/issues/5691">#5691</a>
from epage/custom</li>
<li><a
href="951762db57"><code>951762d</code></a>
feat(complete): Allow any OsString-compatible type to be a
CompletionCandidate</li>
<li><a
href="bb6493e890"><code>bb6493e</code></a>
feat(complete): Offer - as a path option</li>
<li><a
href="27b348dbcb"><code>27b348d</code></a>
refactor(complete): Simplify ArgValueCandidates code</li>
<li><a
href="49b8108f8c"><code>49b8108</code></a>
feat(complete): Add PathCompleter</li>
<li><a
href="82a360aa54"><code>82a360a</code></a>
feat(complete): Add ArgValueCompleter</li>
<li><a
href="47aedc6906"><code>47aedc6</code></a>
fix(complete): Ensure paths are sorted</li>
<li><a
href="431e2bc931"><code>431e2bc</code></a>
test(complete): Ensure ArgValueCandidates get filtered</li>
<li>Additional commits viewable in <a
href="https://github.com/clap-rs/clap/compare/clap_complete-v4.5.20...clap_complete-v4.5.21">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=clap&package-manager=cargo&previous-version=4.5.20&new-version=4.5.21)](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-11-19 20:56:02 -08:00
dependabot[bot]
c8339494a8 chore(deps): bump clap-verbosity-flag from 2.2.2 to 2.2.3 (#1494)
Bumps
[clap-verbosity-flag](https://github.com/clap-rs/clap-verbosity-flag)
from 2.2.2 to 2.2.3.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/clap-rs/clap-verbosity-flag/blob/master/CHANGELOG.md">clap-verbosity-flag's
changelog</a>.</em></p>
<blockquote>
<h2>[2.2.3] - 2024-11-16</h2>
<h3>Features</h3>
<ul>
<li>Add <code>DebugLevel</code> and <code>TraceLevel</code> for
exploratory programming</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="99c0859e7b"><code>99c0859</code></a>
chore: Release clap-verbosity-flag version 2.2.3</li>
<li><a
href="9966b2552c"><code>9966b25</code></a>
docs: Update changelog</li>
<li><a
href="cdd29017d5"><code>cdd2901</code></a>
Merge pull request <a
href="https://redirect.github.com/clap-rs/clap-verbosity-flag/issues/123">#123</a>
from joshka/jm/additional-log-levels</li>
<li><a
href="2192330889"><code>2192330</code></a>
docs: Add example for setting default log level</li>
<li><a
href="e628191254"><code>e628191</code></a>
chore: Remove unnecessary clippy allow directives</li>
<li><a
href="6e5fc6a6be"><code>6e5fc6a</code></a>
feat: Add additional log levels</li>
<li><a
href="a7e305e8b0"><code>a7e305e</code></a>
Merge pull request <a
href="https://redirect.github.com/clap-rs/clap-verbosity-flag/issues/120">#120</a>
from clap-rs/renovate/stable-1.x</li>
<li><a
href="3e3368c681"><code>3e3368c</code></a>
chore(deps): Update Rust crate clap to v4.5.20 (<a
href="https://redirect.github.com/clap-rs/clap-verbosity-flag/issues/119">#119</a>)</li>
<li><a
href="f1c663cba5"><code>f1c663c</code></a>
chore(deps): Update dependency STABLE to v1.82.0</li>
<li><a
href="42bed96a45"><code>42bed96</code></a>
Merge pull request <a
href="https://redirect.github.com/clap-rs/clap-verbosity-flag/issues/118">#118</a>
from clap-rs/renovate/stable-1.x</li>
<li>Additional commits viewable in <a
href="https://github.com/clap-rs/clap-verbosity-flag/compare/v2.2.2...v2.2.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=clap-verbosity-flag&package-manager=cargo&previous-version=2.2.2&new-version=2.2.3)](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-11-19 20:05:42 -08:00
dependabot[bot]
3ef1face9a chore(deps): bump octocrab from 0.41.2 to 0.42.0 (#1498)
Bumps [octocrab](https://github.com/XAMPPRocky/octocrab) from 0.41.2 to
0.42.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/XAMPPRocky/octocrab/releases">octocrab's
releases</a>.</em></p>
<blockquote>
<h2>v0.42.0</h2>
<h3>Added</h3>
<ul>
<li>added ssh_signing_keys ops (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/725">#725</a>)</li>
</ul>
<h3>Other</h3>
<ul>
<li>Secrets and Code scanning alerts API (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/730">#730</a>)</li>
<li>add support for custom executors (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/728">#728</a>)</li>
<li>Fixup route in api/issues/update (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/732">#732</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/XAMPPRocky/octocrab/blob/main/CHANGELOG.md">octocrab's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/XAMPPRocky/octocrab/compare/v0.41.2...v0.42.0">0.42.0</a>
- 2024-11-13</h2>
<h3>Added</h3>
<ul>
<li>added ssh_signing_keys ops (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/725">#725</a>)</li>
</ul>
<h3>Other</h3>
<ul>
<li>Secrets and Code scanning alerts API (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/730">#730</a>)</li>
<li>add support for custom executors (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/728">#728</a>)</li>
<li>Fixup route in api/issues/update (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/732">#732</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="9c39eaac75"><code>9c39eaa</code></a>
chore: release v0.42.0 (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/733">#733</a>)</li>
<li><a
href="5f6b085a62"><code>5f6b085</code></a>
Secrets and Code scanning alerts API (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/730">#730</a>)</li>
<li><a
href="6f30dc5cf1"><code>6f30dc5</code></a>
imp: add support for custom executors (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/728">#728</a>)</li>
<li><a
href="57681d9940"><code>57681d9</code></a>
Fixup route in api/issues/update (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/732">#732</a>)</li>
<li><a
href="3958f288a4"><code>3958f28</code></a>
feat: added ssh_signing_keys ops (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/725">#725</a>)</li>
<li><a
href="c043d063f0"><code>c043d06</code></a>
chore: release v0.41.2 (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/721">#721</a>)</li>
<li>See full diff in <a
href="https://github.com/XAMPPRocky/octocrab/compare/v0.41.2...v0.42.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=octocrab&package-manager=cargo&previous-version=0.41.2&new-version=0.42.0)](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-11-19 20:00:48 -08:00
dependabot[bot]
f4cbab4101 chore(deps): bump codecov/codecov-action from 4 to 5 (#1499)
Bumps
[codecov/codecov-action](https://github.com/codecov/codecov-action) from
4 to 5.
<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>v5.0.0</h2>
<h2>v5 Release</h2>
<p><code>v5</code> of the Codecov GitHub Action will use the <a
href="https://github.com/codecov/wrapper">Codecov Wrapper</a> to
encapsulate the <a
href="https://github.com/codecov/codecov-cli">CLI</a>. This will help
ensure that the Action gets updates quicker.</p>
<h3>Migration Guide</h3>
<p>The <code>v5</code> release also coincides with the opt-out feature
for tokens for public repositories. In the <code>Global Upload
Token</code> section of the settings page of an organization in
codecov.io, you can set the ability for Codecov to receive a coverage
reports from any source. This will allow contributors or other members
of a repository to upload without needing access to the Codecov token.
For more details see <a
href="https://docs.codecov.com/docs/codecov-tokens#uploading-without-a-token">how
to upload without a token</a>.</p>
<blockquote>
<p>[!WARNING]<br />
<strong>The following arguments have been changed</strong></p>
<ul>
<li><code>file</code> (this has been deprecated in favor of
<code>files</code>)</li>
<li><code>plugin</code> (this has been deprecated in favor of
<code>plugins</code>)</li>
</ul>
</blockquote>
<p>The following arguments have been added:</p>
<ul>
<li><code>binary</code></li>
<li><code>gcov_args</code></li>
<li><code>gcov_executable</code></li>
<li><code>gcov_ignore</code></li>
<li><code>gcov_include</code></li>
<li><code>report_type</code></li>
<li><code>skip_validation</code></li>
<li><code>swift_project</code></li>
</ul>
<p>You can see their usage in the <code>action.yml</code> <a
href="https://github.com/codecov/codecov-action/blob/main/action.yml">file</a>.</p>
<h2>What's Changed</h2>
<ul>
<li>chore(deps): bump to eslint9+ and remove eslint-config-google by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1591">codecov/codecov-action#1591</a></li>
<li>build(deps-dev): bump <code>@​octokit/webhooks-types</code> from
7.5.1 to 7.6.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1595">codecov/codecov-action#1595</a></li>
<li>build(deps-dev): bump typescript from 5.6.2 to 5.6.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1604">codecov/codecov-action#1604</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from
8.8.0 to 8.8.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1601">codecov/codecov-action#1601</a></li>
<li>build(deps): bump <code>@​actions/core</code> from 1.11.0 to 1.11.1
by <a href="https://github.com/dependabot"><code>@​dependabot</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1597">codecov/codecov-action#1597</a></li>
<li>build(deps): bump github/codeql-action from 3.26.9 to 3.26.11 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1596">codecov/codecov-action#1596</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code>
from 8.8.0 to 8.8.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1600">codecov/codecov-action#1600</a></li>
<li>build(deps-dev): bump eslint from 9.11.1 to 9.12.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1598">codecov/codecov-action#1598</a></li>
<li>build(deps): bump github/codeql-action from 3.26.11 to 3.26.12 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1609">codecov/codecov-action#1609</a></li>
<li>build(deps): bump actions/checkout from 4.2.0 to 4.2.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1608">codecov/codecov-action#1608</a></li>
<li>build(deps): bump actions/upload-artifact from 4.4.0 to 4.4.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1607">codecov/codecov-action#1607</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from
8.8.1 to 8.9.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1612">codecov/codecov-action#1612</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code>
from 8.8.1 to 8.9.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1611">codecov/codecov-action#1611</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code>
from 8.9.0 to 8.10.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1615">codecov/codecov-action#1615</a></li>
<li>build(deps-dev): bump eslint from 9.12.0 to 9.13.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1618">codecov/codecov-action#1618</a></li>
<li>build(deps): bump github/codeql-action from 3.26.12 to 3.26.13 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1617">codecov/codecov-action#1617</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from
8.9.0 to 8.10.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1614">codecov/codecov-action#1614</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code>
from 8.10.0 to 8.11.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1620">codecov/codecov-action#1620</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from
8.10.0 to 8.11.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1619">codecov/codecov-action#1619</a></li>
<li>build(deps-dev): bump <code>@​types/jest</code> from 29.5.13 to
29.5.14 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1622">codecov/codecov-action#1622</a></li>
<li>build(deps): bump actions/checkout from 4.2.1 to 4.2.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1625">codecov/codecov-action#1625</a></li>
<li>build(deps): bump github/codeql-action from 3.26.13 to 3.27.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1624">codecov/codecov-action#1624</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code>
from 8.11.0 to 8.12.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1626">codecov/codecov-action#1626</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code>
from 8.12.1 to 8.12.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1629">codecov/codecov-action#1629</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="5c47607acb"><code>5c47607</code></a>
fix: override commit and pr values for PR cases (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1657">#1657</a>)</li>
<li><a
href="3b1354a6c4"><code>3b1354a</code></a>
chore(release): 5.0.1 (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1656">#1656</a>)</li>
<li><a
href="2e2a9c6d58"><code>2e2a9c6</code></a>
fix: update tokenless branch logic (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1650">#1650</a>)</li>
<li><a
href="cfc521b7a1"><code>cfc521b</code></a>
Update README.md</li>
<li><a
href="06425412c8"><code>0642541</code></a>
fix: use marketplace v5 badge (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1646">#1646</a>)</li>
<li><a
href="968872560f"><code>9688725</code></a>
Update README.md</li>
<li><a
href="2112eaec1b"><code>2112eae</code></a>
chore(deps): bump wrapper to 0.0.23 (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1644">#1644</a>)</li>
<li><a
href="193421c5b3"><code>193421c</code></a>
fixL use the correct source (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1642">#1642</a>)</li>
<li><a
href="6018df70b0"><code>6018df7</code></a>
fix: update container builds (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1640">#1640</a>)</li>
<li><a
href="eff1a643d6"><code>eff1a64</code></a>
fix: add missing vars (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1638">#1638</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/codecov/codecov-action/compare/v4...v5">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=4&new-version=5)](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-11-19 19:52:52 -08:00
dependabot[bot]
ae6a8501ee chore(deps): bump DavidAnson/markdownlint-cli2-action from 17 to 18 (#1500)
Bumps
[DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action)
from 17 to 18.
<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.15.0, markdownlint
v0.36.1).</h2>
<p>No release notes provided.</p>
<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="eb5ca3ab41"><code>eb5ca3a</code></a>
Update to version 18.0.0.</li>
<li><a
href="bd47e95879"><code>bd47e95</code></a>
Freshen generated index.js file.</li>
<li><a
href="ad0fecff0d"><code>ad0fecf</code></a>
Bump markdownlint-cli2 from 0.14.0 to 0.15.0</li>
<li><a
href="079995c6c3"><code>079995c</code></a>
Bump eslint-plugin-n from 17.13.0 to 17.13.1</li>
<li><a
href="4163a2f0f6"><code>4163a2f</code></a>
Bump eslint-plugin-n from 17.12.0 to 17.13.0</li>
<li><a
href="8b67109ec6"><code>8b67109</code></a>
Bump eslint from 9.13.0 to 9.14.0</li>
<li><a
href="f6d787a2fc"><code>f6d787a</code></a>
Bump <code>@​eslint/js</code> from 9.13.0 to 9.14.0</li>
<li><a
href="c05e13d409"><code>c05e13d</code></a>
Bump <code>@​stylistic/eslint-plugin</code> from 2.10.0 to 2.10.1</li>
<li><a
href="6d12e16c76"><code>6d12e16</code></a>
Bump eslint-plugin-n from 17.11.1 to 17.12.0</li>
<li><a
href="0f558ed3a6"><code>0f558ed</code></a>
Bump <code>@​stylistic/eslint-plugin</code> from 2.9.0 to 2.10.0</li>
<li>Additional commits viewable in <a
href="https://github.com/davidanson/markdownlint-cli2-action/compare/v17...v18">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=17&new-version=18)](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-11-19 19:52:23 -08:00
dependabot[bot]
1bb41e7165 chore(deps): bump instability from 0.3.2 to 0.3.3 (#1497)
Bumps [instability](https://github.com/ratatui-org/instability) from
0.3.2 to 0.3.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ratatui-org/instability/releases">instability's
releases</a>.</em></p>
<blockquote>
<h2>instability-v0.3.3</h2>
<h3>Added</h3>
<ul>
<li>add stable macro (<a
href="https://redirect.github.com/ratatui/instability/pull/14">#14</a>)</li>
<li>use doc(cfg)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>tests (<a
href="https://redirect.github.com/ratatui/instability/pull/13">#13</a>)</li>
<li>change master to main in lib.rs</li>
</ul>
<h3>Other</h3>
<ul>
<li>bump msrv to 1.63</li>
<li>use proc_macro2 and add tests</li>
<li>use darling instead of manual parsing for better error messages on
attributes</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/ratatui/instability/blob/main/CHANGELOG.md">instability's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/ratatui/instability/compare/instability-v0.3.2...instability-v0.3.3">0.3.3</a>
- 2024-11-12</h2>
<h3>Added</h3>
<ul>
<li>add stable macro (<a
href="https://redirect.github.com/ratatui/instability/pull/14">#14</a>)</li>
<li>use doc(cfg)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>tests (<a
href="https://redirect.github.com/ratatui/instability/pull/13">#13</a>)</li>
<li>change master to main in lib.rs</li>
</ul>
<h3>Other</h3>
<ul>
<li>bump msrv to 1.63</li>
<li>use proc_macro2 and add tests</li>
<li>use darling instead of manual parsing for better error messages on
attributes</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="e365305716"><code>e365305</code></a>
chore: release v0.3.3 (<a
href="https://redirect.github.com/ratatui-org/instability/issues/9">#9</a>)</li>
<li><a
href="14f2993a8f"><code>14f2993</code></a>
feat: add stable macro (<a
href="https://redirect.github.com/ratatui-org/instability/issues/14">#14</a>)</li>
<li><a
href="f833f10e01"><code>f833f10</code></a>
fix: tests (<a
href="https://redirect.github.com/ratatui-org/instability/issues/13">#13</a>)</li>
<li><a
href="261fdfd252"><code>261fdfd</code></a>
build: bump msrv to 1.63</li>
<li><a
href="402d37064c"><code>402d370</code></a>
feat: use doc(cfg)</li>
<li><a
href="731685a257"><code>731685a</code></a>
refactor: use proc_macro2 and add tests</li>
<li><a
href="82d15c721c"><code>82d15c7</code></a>
chore: use darling instead of manual parsing for better error messages
on att...</li>
<li><a
href="47e02c2009"><code>47e02c2</code></a>
fix: change master to main in lib.rs</li>
<li>See full diff in <a
href="https://github.com/ratatui-org/instability/compare/instability-v0.3.2...instability-v0.3.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=instability&package-manager=cargo&previous-version=0.3.2&new-version=0.3.3)](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-11-19 19:31:38 -08:00
thscharler
4d7704fba5 fix: Make StatefulWidget and Ref work with unsized State (#1505)
StatefulWidget::State and StatefulWidgetRef::State are now ?Sized.

This allows implementations of the traits to use unsized types for the
State associated type. This is turn is useful when doing things like
boxing different stateful widget types with State which implements
`Any`, are slices or any other dynamically sized type.

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-11-19 17:56:26 -08:00
Josh McKinney
e4e95bcecf chore: remove --color always flags from bacon.toml (#1502)
No longer necessary as of bacon 3.3
2024-11-19 21:21:12 +03:00
Josh McKinney
a41c97b413 chore: move unstable widget refs to ratatui (#1491)
These are less stable than the non-ref traits as we have not yet
committed to the exact API. This change moves them to ratatui from
ratatui-core.

To facilitate this:
- implementations of WidgetRef for all internal widgets are removed and
  replaced with implementations of Widget for references to those
  widgets.
- Widget is now implemented for Option<W> where W: Widget, allowing for
  rendering of optional widgets.
- The blanket implementation of Widget for WidgetRef is reversed, to be
  a blanket implementation of WidgetRef for all &W where W: Widget.

BREAKING CHANGE: implementations of WidgetRef no longer have a blanket
implementation of Widget, so Widgets should generally implement the
Widget trait on a reference to the widget rather than implementing
WidgetRef directly. This has the advantage of not requiring unstable
features to be enabled.

Part of: https://github.com/ratatui/ratatui/issues/1388

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-11-18 14:19:21 -08:00
Orhun Parmaksız
46902f5587 docs: improve docs for workspace crates (#1490)
Overall makes improvements in the documentation of the workspace crates and checking them.
2024-11-18 02:03:44 +03:00
Josh McKinney
e7085e3a3e chore: move widgets into ratatui-widgets crate (#1474)
All the widgets now live in their own ratatui-widgets crate, but are re-exported in the main ratatui crate.
This makes it easier to use portions of the ratatui library and is part of the effort to modularize

Part of: #1388

---------

Co-authored-by: Orhun Parmaksız <orhun@archlinux.org>
Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-11-15 19:42:07 -08:00
Nils Martel
9f90f7495f docs(readme): fix broken link (#1485) 2024-11-13 12:52:40 +03:00
Dheepak Krishnamurthy
260af68a34 docs(readme): include iocraft as an alternative (#1483) 2024-11-12 18:41:43 +03:00
Josh McKinney
e461b724a6 refactor: move {Stateful,}Widget{,Ref} types into individual files (#1479)
This is a preparatory refactoring for modularization. No user visible
changes.
2024-11-12 15:34:48 +03:00
dependabot[bot]
02c8c9373e chore(deps): bump unicode-truncate from 1.1.0 to 2.0.0 (#1481)
Bumps [unicode-truncate](https://github.com/Aetf/unicode-truncate) from
1.1.0 to 2.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/Aetf/unicode-truncate/releases">unicode-truncate's
releases</a>.</em></p>
<blockquote>
<h2>v2.0.0</h2>
<h3>Fixed</h3>
<ul>
<li><em>(deps)</em> update rust crate unicode-width to 0.2</li>
</ul>
<h3>Other</h3>
<ul>
<li>make release-plz use github app token</li>
<li>[<strong>breaking</strong>] bump MSRV to 1.66</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/Aetf/unicode-truncate/blob/master/CHANGELOG.md">unicode-truncate's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/Aetf/unicode-truncate/compare/v1.1.0...v2.0.0">2.0.0</a>
- 2024-11-10</h2>
<h3>Fixed</h3>
<ul>
<li><em>(deps)</em> update rust crate unicode-width to 0.2</li>
</ul>
<h3>Other</h3>
<ul>
<li>make release-plz use github app token</li>
<li>[<strong>breaking</strong>] bump MSRV to 1.66</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="95b4d26103"><code>95b4d26</code></a>
chore: release v2.0.0</li>
<li><a
href="4c0b472226"><code>4c0b472</code></a>
ci: make release-plz use github app token</li>
<li><a
href="2812162513"><code>2812162</code></a>
build!: bump MSRV to 1.66</li>
<li><a
href="8984d1529a"><code>8984d15</code></a>
fix(deps): update rust crate unicode-width to 0.2</li>
<li><a
href="abd6d916b9"><code>abd6d91</code></a>
fix(deps): update rust crate unicode-segmentation to v1.12.0</li>
<li>See full diff in <a
href="https://github.com/Aetf/unicode-truncate/compare/v1.1.0...v2.0.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=unicode-truncate&package-manager=cargo&previous-version=1.1.0&new-version=2.0.0)](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-11-12 03:42:47 -08:00
dependabot[bot]
f40fa787d1 chore(deps): bump tokio from 1.40.0 to 1.41.1 (#1482)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.40.0 to 1.41.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/tokio-rs/tokio/releases">tokio's
releases</a>.</em></p>
<blockquote>
<h2>Tokio v1.41.1</h2>
<h1>1.41.1 (Nov 7th, 2024)</h1>
<h3>Fixed</h3>
<ul>
<li>metrics: fix bug with wrong number of buckets for the histogram (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6957">#6957</a>)</li>
<li>net: display <code>net</code> requirement for
<code>net::UdpSocket</code> in docs (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6938">#6938</a>)</li>
<li>net: fix typo in <code>TcpStream</code> internal comment (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6944">#6944</a>)</li>
</ul>
<p><a
href="https://redirect.github.com/tokio-rs/tokio/issues/6957">#6957</a>:
<a
href="https://redirect.github.com/tokio-rs/tokio/pull/6957">tokio-rs/tokio#6957</a>
<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6938">#6938</a>:
<a
href="https://redirect.github.com/tokio-rs/tokio/pull/6938">tokio-rs/tokio#6938</a>
<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6944">#6944</a>:
<a
href="https://redirect.github.com/tokio-rs/tokio/pull/6944">tokio-rs/tokio#6944</a></p>
<h2>Tokio v1.41.0</h2>
<h1>1.41.0 (Oct 22th, 2024)</h1>
<h3>Added</h3>
<ul>
<li>metrics: stabilize <code>global_queue_depth</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6854">#6854</a>,
<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6918">#6918</a>)</li>
<li>net: add conversions for unix <code>SocketAddr</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6868">#6868</a>)</li>
<li>sync: add <code>watch::Sender::sender_count</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6836">#6836</a>)</li>
<li>sync: add <code>mpsc::Receiver::blocking_recv_many</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6867">#6867</a>)</li>
<li>task: stabilize <code>Id</code> apis (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6793">#6793</a>,
<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6891">#6891</a>)</li>
</ul>
<h3>Added (unstable)</h3>
<ul>
<li>metrics: add H2 Histogram option to improve histogram granularity
(<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6897">#6897</a>)</li>
<li>metrics: rename some histogram apis (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6924">#6924</a>)</li>
<li>runtime: add <code>LocalRuntime</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6808">#6808</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>runtime: box futures larger than 16k on release mode (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6826">#6826</a>)</li>
<li>sync: add <code>#[must_use]</code> to <code>Notified</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6828">#6828</a>)</li>
<li>sync: make <code>watch</code> cooperative (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6846">#6846</a>)</li>
<li>sync: make <code>broadcast::Receiver</code> cooperative (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6870">#6870</a>)</li>
<li>task: add task size to tracing instrumentation (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6881">#6881</a>)</li>
<li>wasm: enable <code>cfg_fs</code> for <code>wasi</code> target (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6822">#6822</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>net: fix regression of abstract socket path in unix socket (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6838">#6838</a>)</li>
</ul>
<h3>Documented</h3>
<ul>
<li>io: recommend <code>OwnedFd</code> with <code>AsyncFd</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6821">#6821</a>)</li>
<li>io: document cancel safety of <code>AsyncFd</code> methods (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6890">#6890</a>)</li>
<li>macros: render more comprehensible documentation for
<code>join</code> and <code>try_join</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6814">#6814</a>,
<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6841">#6841</a>)</li>
<li>net: fix swapped examples for <code>TcpSocket::set_nodelay</code>
and <code>TcpSocket::nodelay</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6840">#6840</a>)</li>
<li>sync: document runtime compatibility (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6833">#6833</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="bb7ca7507b"><code>bb7ca75</code></a>
chore: prepare Tokio v1.41.1 (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6959">#6959</a>)</li>
<li><a
href="4a34b77af5"><code>4a34b77</code></a>
metrics: fix bug with wrong number of buckets for the histogram (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6957">#6957</a>)</li>
<li><a
href="8897885425"><code>8897885</code></a>
docs: fix mismatched backticks in CONTRIBUTING.md (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6951">#6951</a>)</li>
<li><a
href="0dbdd196b6"><code>0dbdd19</code></a>
ci: update cargo-check-external-types to 0.1.13 (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6949">#6949</a>)</li>
<li><a
href="94e55c092b"><code>94e55c0</code></a>
net: fix typo in <code>TcpStream</code> internal comment (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6944">#6944</a>)</li>
<li><a
href="4468f27c31"><code>4468f27</code></a>
metrics: fixed flaky <code>worker_steal_count</code> test (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6932">#6932</a>)</li>
<li><a
href="070a825999"><code>070a825</code></a>
metrics: removed race condition from global_queue_depth_multi_thread
test (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6">#6</a>...</li>
<li><a
href="946401c345"><code>946401c</code></a>
net: display <code>net</code> requirement for
<code>net::UdpSocket</code> in docs (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6938">#6938</a>)</li>
<li><a
href="0c01fd23b4"><code>0c01fd2</code></a>
ci: use patched version of cargo-check-external-types to fix CI failure
(<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6937">#6937</a>)</li>
<li><a
href="ebe241647e"><code>ebe2416</code></a>
ci: use cargo deny (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/6931">#6931</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/tokio-rs/tokio/compare/tokio-1.40.0...tokio-1.41.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tokio&package-manager=cargo&previous-version=1.40.0&new-version=1.41.1)](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-11-12 03:42:08 -08:00
Marco Ieni
7b875091e1 fix: typo (#1480) 2024-11-09 19:10:31 -08:00
Orhun Parmaksız
17316ec5d0 chore(github): enable sponsorship button (#1478) 2024-11-08 16:34:03 +03:00
Orhun Parmaksız
eaa403856e chore(ci): install pre-built binaries for cargo-rdme (#1477)
`install-action` uses `cargo-binstall` to install the pre-built binaries
of `cargo-rdme` (which was released in
https://github.com/orium/cargo-rdme/releases/tag/v1.4.7).

This will make the `check-readme` step faster in CI (now takes only 10
seconds).
2024-11-08 16:33:48 +03:00
Orhun Parmaksız
e5e2316451 chore(ci): add check for keeping README.md up-to-date (#1473) 2024-11-07 09:45:14 +03:00
Josh McKinney
98df774d7f chore(core): move core types to ratatui-core (#1460)
The buffer, layout, style, symbols, text, and the top level of widgets
modules are moved to ratatui-core. This is the first step in
modularizing the library so that the core types can be used in other
projects without the need for the backend / widgets types.

This helps reduce the need for updating other crates as often due to
semver changes outside of the core types.

---------

Co-authored-by: Orhun Parmaksız <orhun@archlinux.org>
Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-11-06 09:10:49 +03:00
Everett Pompeii
0a47ebd94b chore(bencher): update bencher CLI usage (#1470) 2024-11-06 00:37:23 +03:00
Josh McKinney
abe2f27328 chore(backend): change From<T> impls to new backend specific IntoBackend and FromBackend traits (#1464)
Adds two traits `IntoCrossterm` and `FromCrossterm` for converting
between ratatui and crossterm types. This is necessary in order to avoid
the orphan rule when implementing `From` for crossterm types once the
crossterm types are moved to a separate crate.

Similarly Termwiz and Termwiz gain FromTermion, IntoTermion, FromTermwiz
and IntoTermwiz traits.

BREAKING CHANGE: The `From` and `Into` impls for backend types are now
replaced
with specific backend traits.

```diff
+ use ratatui::backend::{FromCrossterm, IntoCrossterm};

let crossterm_color = crossterm::style::Color::Black;
- let ratatui_color = crossterm_color.into();
- let ratatui_color = ratatui::style::Color::from(crossterm_color);
+ let ratatui_color = ratatui::style::Color::from_crossterm(crossterm_color);
- let crossterm_color = ratatui_color.into();
- let crossterm_color = crossterm::style::Color::from(ratatui_color);
+ let crossterm_color = ratatui_color.into_crossterm();

let crossterm_attribute = crossterm::style::types::Attribute::Bold;
- let ratatui_modifier = crossterm_attribute.into();
- let ratatui_modifier = ratatui::style::Modifier::from(crossterm_attribute);
+ let ratatui_modifier = ratatui::style::Modifier::from_crossterm(crossterm_attribute);
- let crossterm_attribute = ratatui_modifier.into();
- let crossterm_attribute = crossterm::style::types::Attribute::from(ratatui_modifier);
+ let crossterm_attribute = ratatui_modifier.into_crossterm();
```

Similar conversions for `ContentStyle` -> `Style` and `Attributes` ->
`Modifier` exist for Crossterm,
and all the Termion and Termwiz types.

---------

Co-authored-by: Orhun Parmaksız <orhun@archlinux.org>
2024-11-03 13:43:24 -08:00
Orhun Parmaksız
fcde9cb9c3 docs(changelog): fix typo (#1463) 2024-11-02 04:28:37 -07:00
Josh McKinney
2ef3583eff chore(ci): replace cargo-make with a custom cargo-xtask (#1461)
This removes the need for cargo-make and replaces it with a custom xtask
binary. See <https://github.com/matklad/cargo-xtask> for info.

Rearranges the CI workflow to use the new xtask and simplify which
workflows that run.

---------

Co-authored-by: Orhun Parmaksız <orhun@archlinux.org>
2024-11-02 10:03:44 +03:00
Thomas TACQUET
a6b579223f docs: fix example link in readme (#1462) 2024-11-01 18:08:07 -07:00
Josh McKinney
f1d0a18375 chore: move ratatui crate into workspace folder (#1459)
This is the first step towards modularization. Handling the move
as a separate step rather than combining it should make it easier
to rebase other PRs when necessary.

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-10-31 14:59:17 -07:00
Josh McKinney
55fb2d2e56 chore: update repo links to ratatui instead of ratatui-org (#1458) 2024-10-30 03:37:04 -07:00
dependabot[bot]
836634734f chore(deps): bump serde from 1.0.210 to 1.0.213 (#1455)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.210 to
1.0.213.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/serde-rs/serde/releases">serde's
releases</a>.</em></p>
<blockquote>
<h2>v1.0.213</h2>
<ul>
<li>Fix support for macro-generated <code>with</code> attributes inside
a newtype struct (<a
href="https://redirect.github.com/serde-rs/serde/issues/2847">#2847</a>)</li>
</ul>
<h2>v1.0.212</h2>
<ul>
<li>Fix hygiene of macro-generated local variable accesses in
serde(with) wrappers (<a
href="https://redirect.github.com/serde-rs/serde/issues/2845">#2845</a>)</li>
</ul>
<h2>v1.0.211</h2>
<ul>
<li>Improve error reporting about mismatched signature in
<code>with</code> and <code>default</code> attributes (<a
href="https://redirect.github.com/serde-rs/serde/issues/2558">#2558</a>,
thanks <a
href="https://github.com/Mingun"><code>@​Mingun</code></a>)</li>
<li>Show variant aliases in error message when variant deserialization
fails (<a
href="https://redirect.github.com/serde-rs/serde/issues/2566">#2566</a>,
thanks <a
href="https://github.com/Mingun"><code>@​Mingun</code></a>)</li>
<li>Improve binary size of untagged enum and internally tagged enum
deserialization by about 12% (<a
href="https://redirect.github.com/serde-rs/serde/issues/2821">#2821</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="58a8d22931"><code>58a8d22</code></a>
Release 1.0.213</li>
<li><a
href="ef0ed22593"><code>ef0ed22</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/serde/issues/2847">#2847</a>
from dtolnay/newtypewith</li>
<li><a
href="79925ac394"><code>79925ac</code></a>
Ignore dead_code warning in regression test</li>
<li><a
href="b60e4092ec"><code>b60e409</code></a>
Hygiene for macro-generated newtype struct deserialization with 'with'
attr</li>
<li><a
href="fdc36e5c06"><code>fdc36e5</code></a>
Add regression test for issue 2846</li>
<li><a
href="49e11ce1ba"><code>49e11ce</code></a>
Ignore trivially_copy_pass_by_ref pedantic clippy lint in test</li>
<li><a
href="7ae1b5f8f3"><code>7ae1b5f</code></a>
Release 1.0.212</li>
<li><a
href="1ac054b34a"><code>1ac054b</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/serde/issues/2845">#2845</a>
from dtolnay/withlocal</li>
<li><a
href="1e36ef551d"><code>1e36ef5</code></a>
Fix hygiene of macro-generated local variable accesses in serde(with)
wrappers</li>
<li><a
href="0058c7226e"><code>0058c72</code></a>
Add regression test for issue 2844</li>
<li>Additional commits viewable in <a
href="https://github.com/serde-rs/serde/compare/v1.0.210...v1.0.213">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=serde&package-manager=cargo&previous-version=1.0.210&new-version=1.0.213)](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-10-28 23:10:57 +03:00
Josh McKinney
860e48b0f0 fix(buffer): buffer::get_pos() now correctly handles index > u16::MAX (#1447)
Previously this function wrapped the index pass u16::MAX which caused
problems rendering.

Fixes: <https://github.com/ratatui/ratatui/issues/1441>
2024-10-22 03:46:27 -07:00
miro
04e1b32cd2 docs(layout): rename cassowary-rs references to cassowary (#1448)
closes #1423
2024-10-22 12:15:36 +03:00
Orhun Parmaksız
28732176e1 chore(release): prepare for 0.29.0 (#1444)
🧀
2024-10-21 13:35:36 +03:00
Josh McKinney
6515097434 chore(cargo): check in Cargo.lock (#1434)
When kept up to date, this makes it possible to build any git version
with the same versions of crates that were used for any version, without
it, you can only use the current versions. This makes bugs in semver
compatible code difficult to detect.

The Cargo.lock file is not used by downstream consumers of the crate, so
it is safe to include it in the repository (and recommended by the Rust
docs).

See:
- https://doc.rust-lang.org/cargo/faq.html#why-have-cargolock-in-version-control
- https://blog.rust-lang.org/2023/08/29/committing-lockfiles.html
- https://github.com/rust-lang/cargo/issues/8728

Co-authored-by: Orhun Parmaksız <orhun@archlinux.org>
2024-10-20 11:59:07 +03:00
Orhun Parmaksız
4c4851ca3d feat(example): add drawing feature to the canvas example (#1429)
![rec_20241018T235208](https://github.com/user-attachments/assets/cfb2f9f8-773b-4599-9312-29625ff2ca60)


fun fact: I had to do [35
pushups](https://www.youtube.com/watch?v=eS92stzBYXA) for this...

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-10-20 01:41:40 -07:00
Josh McKinney
4f5503dbf6 fix(color)!: hsl and hsluv are now clamped before conversion (#1436)
The `from_hsl` and `from_hsluv` functions now clamp the HSL and HSLuv
values before converting them to RGB. This ensures that the input values
are within the expected range before conversion.

Also note that the ranges of Saturation and Lightness values have been
aligned to be consisten with the palette crate. Saturation and Lightness
for `from_hsl` are now in the range [0.0..1.0] while `from_hsluv` are
in the range [0.0..100.0].

Refs:
- <https://github.com/Ogeon/palette/discussions/253>
- <https://docs.rs/palette/latest/palette/struct.Hsl.html>
- <https://docs.rs/palette/latest/palette/struct.Hsluv.html>

Fixes: <https://github.com/ratatui/ratatui/issues/1433>
2024-10-20 00:46:04 -07:00
Josh McKinney
611086eba4 fix: sparkline docs / doc tests (#1437) 2024-10-20 09:24:38 +03:00
Robert Hensing
514d273875 fix(terminal): use the latest, resized area when clearing (#1427) 2024-10-19 20:17:12 -07:00
FujiApple
60cc15bbb0 feat!: add support for empty bar style to Sparkline (#1326)
- distingiush between empty bars and bars with a value of 0
- provide custom styling for empty bars
- provide custom styling for individual bars
- inverts the rendering algorithm to be item first

Closes: #1325 

BREAKING CHANGE: `Sparkline::data` takes `IntoIterator<Item = SparklineBar>`
instead of `&[u64]` and is no longer const

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-10-19 19:49:05 -07:00
Eric Lunderberg
a52ee82fc7 fix(text): truncate based on alignment (#1432)
This is a follow-up PR to https://github.com/ratatui/ratatui/pull/987,
which implemented alignment-aware truncation for the `Line` widget.
However, the truncation only checked the `Line::alignment` field, and
any alignment inherited from a parent's `Text::alignment` field would
not be used.

This commit updates the truncation of `Line` to depend both on the
individual `Line::alignment`, and on any alignment inherited from the
parent's `Text::alignment`.

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-10-19 09:16:44 -07:00
Josh McKinney
381ec75329 docs(readme): reduce the length (#1431)
Motivation for this is that there's a bunch of stuff at the bottom of the Readme that we don't really keep up to date. Instead it's better to link to the places that we do keep this info.
2024-10-19 16:28:04 +03:00
Josh McKinney
f6f7794dd7 chore: remove leftover prelude refs / glob imports from example code (#1430)
Fixes: <https://github.com/ratatui/ratatui/issues/1150>

Co-authored-by: Dheepak Krishnamurthy <1813121+kdheepak@users.noreply.github.com>
2024-10-19 02:54:34 -07:00
Dheepak Krishnamurthy
453a308b46 feat!: Add overlap to layout (#1398)
This PR adds a new feature for the existing `Layout::spacing` method,
and introducing a `Spacing` enum.

Now `Layout::spacing` is generic and can take

- zero or positive numbers, e.g. `Layout::spacing(1)` (current
functionality)
- negative number, e.g. `Layout::spacing(-1)` (new)
- variant of the `Spacing` (new)

This allows creating layouts with a shared pixel for segments. When
`spacing(negative_value)` is used, spacing is ignored and all segments
will be adjacent and have pixels overlapping.
`spacing(zero_or_positive_value)` behaves the same as before. These are
internally converted to `Spacing::Overlap` or `Spacing::Space`.

Here's an example output to illustrate the layout solve from this PR:

```rust
#[test]
fn test_layout() {
    use crate::layout::Constraint::*;
    let mut terminal = crate::Terminal::new(crate::backend::TestBackend::new(50, 4)).unwrap();
    terminal
        .draw(|frame| {
            let [upper, lower] = Layout::vertical([Fill(1), Fill(1)]).areas(frame.area());

            let (segments, spacers) = Layout::horizontal([Length(10), Length(10), Length(10)])
                .flex(Flex::Center)
                .split_with_spacers(upper);

            for segment in segments.iter() {
                frame.render_widget(
                    crate::widgets::Block::bordered()
                        .border_set(crate::symbols::border::DOUBLE),
                    *segment,
                );
            }
            for spacer in spacers.iter() {
                frame.render_widget(crate::widgets::Block::bordered(), *spacer);
            }

            let (segments, spacers) = Layout::horizontal([Length(10), Length(10), Length(10)])
                .flex(Flex::Center)
                .spacing(-1) // new feature
                .split_with_spacers(lower);

            for segment in segments.iter() {
                frame.render_widget(
                    crate::widgets::Block::bordered()
                        .border_set(crate::symbols::border::DOUBLE),
                    *segment,
                );
            }
            for spacer in spacers.iter() {
                frame.render_widget(crate::widgets::Block::bordered(), *spacer);
            }
        })
        .unwrap();
    dbg!(terminal.backend());
}
```


```plain
┌────────┐╔════════╗╔════════╗╔════════╗┌────────┐
└────────┘╚════════╝╚════════╝╚════════╝└────────┘
┌─────────┐╔════════╔════════╔════════╗┌─────────┐
└─────────┘╚════════╚════════╚════════╝└─────────┘
```

Currently drawing a border on top of an existing border overwrites it.
Future PRs will allow for making the border drawing handle overlaps
better.

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-10-16 05:41:08 -04:00
Josh McKinney
9fd1beedb2 chore!: make Positions iterator fields private (#1424)
BREAKING CHANGE: The Rect Positions iterator no longer has public
fields. The `rect` and `current_position` fields have been made private
as they were not intended to be accessed directly.
2024-10-15 21:38:53 -07:00
Tayfun Bocek
8db7a9a44a perf: implement size hints for Rect iterators (#1420)
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-10-15 18:48:17 -07:00
Josh McKinney
b7e488507d fix(color): fix doc test for from_hsl (#1421) 2024-10-15 13:08:52 +03:00
Josh McKinney
4728f0e68b docs: tweak readme (#1419)
Fixes: <https://github.com/ratatui/ratatui/issues/1417>
2024-10-15 03:00:03 -07:00
Orhun Parmaksız
6db16d67fc refactor(color)!: use palette types for Hsl/Hsluv conversions (#1418)
BREAKING-CHANGE: Previously `Color::from_hsl` accepted components
as individual f64 parameters. It now accepts a single `palette::Hsl`
value
and is gated behind a `palette` feature flag.

```diff
- Color::from_hsl(360.0, 100.0, 100.0)
+ Color::from_hsl(Hsl::new(360.0, 100.0, 100.0))
```

Fixes: <https://github.com/ratatui/ratatui/issues/1414>

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-10-14 19:15:05 -07:00
Orhun Parmaksız
cc7497532a chore(deps)!: pin unicode-width to 0.2.0 (#1403)
We pin unicode-width to avoid breaking applications when there are breaking changes in the library.

Discussion in #1271

Fixes: #1385

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-10-14 19:05:43 -07:00
Neal Fachan
d72968d86b feat(scrolling-regions)!: use terminal scrolling regions to stop Terminal::insert_before from flickering (#1341)
The current implementation of Terminal::insert_before causes the
viewport to flicker. This is described in #584 .

This PR removes that flickering by using terminal scrolling regions
(sometimes called "scroll regions"). A terminal can have its scrolling
region set to something other than the whole screen. When a scroll ANSI
sequence is sent to the terminal and it has a non-default scrolling
region, the terminal will scroll just inside of that region.

We use scrolling regions to implement insert_before. We create a region
on the screen above the viewport, scroll that up to make room for the
newly inserted lines, and then draw the new lines. We may need to repeat
this process depending on how much space there is and how many lines we
need to draw.

When the viewport takes up the entire screen, we take a modified
approach. We create a scrolling region of just the top line (could be
more) of the viewport, then use that to draw the lines we want to
output. When we're done, we scroll it up by one line, into the
scrollback history, and then redraw the top line from the viewport.

A final edge case is when the viewport hasn't yet reached the bottom of
the screen. This case, we set up a different scrolling region, where the
top is the top of the viewport, and the bottom is the viewport's bottom
plus the number of lines we want to scroll by. We then scroll this
region down to open up space above the viewport for drawing the inserted
lines.

Regardless of what we do, we need to reset the scrolling region. This PR
takes the approach of always resetting the scrolling region after every
operation. So the Backend gets new scroll_region_up and
scroll_region_down methods instead of set_scrolling_region, scroll_up,
scroll_down, and reset_scrolling_region methods. We chose that approach
for two reasons. First, we don't want Ratatui to have to remember that
state and then reset the scrolling region when tearing down. Second, the
pre-Windows-10 console code doesn't support scrolling regio

This PR:
- Adds a new scrolling-regions feature.
- Adds two new Backend methods: scroll_region_up and scroll_region_down.
- Implements those Backend methods on all backends in the codebase.
- The crossterm and termion implementations use raw ANSI escape
sequences. I'm trying to merge changes into those two projects
separately to support these functions.
- Adds code to Terminal::insert_before to choose between
insert_before_scrolling_regions and insert_before_no_scrolling_regions.
The latter is the old implementation.
- Adds lots of tests to the TestBackend to for the
scrolling-region-related Backend methods.
- Adds versions of terminal tests that show that insert_before doesn't
clobber the viewport. This is a change in behavior from before.
2024-10-14 15:46:13 -07:00
FujiApple
7bdccce3d5 feat!: add an impl of DoubleEndedIterator for Columns and Rows (#1358)
BREAKING-CHANGE: The `pub` modifier has been removed from fields on the
`layout::rect::Columns` and `layout::rect::Rows` iterators. These fields
were not intended to be public and should not have been accessed
directly.

Fixes: #1357
2024-10-14 15:41:39 -07:00
Josh McKinney
3df685e114 fix(rect)!: Rect::area now returns u32 and Rect::new() no longer clamps area to u16::MAX (#1378)
This change fixes the unexpected behavior of the Rect::new() function to
be more intuitive. The Rect::new() function now clamps the width and
height of the rectangle to keep each bound within u16::MAX. The
Rect::area() function now returns a u32 instead of a u16 to allow for
larger areas to be calculated.

Previously, the Rect::new() function would clamp the total area of the
rectangle to u16::MAX, by preserving the aspect ratio of the rectangle.

BREAKING CHANGE: Rect::area() now returns a u32 instead of a u16.

Fixes: <https://github.com/ratatui/ratatui/issues/1375>

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-10-14 15:04:56 -07:00
Josh McKinney
4069aa8274 docs: fix missing breaking changes link (#1416) 2024-10-14 17:46:23 +03:00
Josh McKinney
e5a7609588 feat(line)!: impl From<Cow<str>> for Line (#1373)
BREAKING-CHANGES: `Line` now implements `From<Cow<str>`

As this adds an extra conversion, ambiguous infered values may no longer
compile.

```rust
// given:
struct Foo { ... }
impl From<Foo> for String { ... }
impl From<Foo> for Cow<str> { ... }

let foo = Foo { ... };
let line = Line::from(foo); // now fails due to ambiguous type inference
// replace with
let line = Line::from(String::from(foo));
```

Fixes: https://github.com/ratatui/ratatui/issues/1367

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-10-14 17:02:57 +03:00
Orhun Parmaksız
69e0cd2fc4 chore(deny): allow Zlib license in cargo-deny configuration (#1411) 2024-10-14 02:48:30 -07:00
Josh McKinney
ab6b1feaec feat(tabs)!: allow tabs to be deselected (#1413)
`Tabs::select()` now accepts `Into<Option<usize>>` instead of `usize`.
This allows tabs to be deselected by passing `None`.

`Tabs::default()` is now also implemented manually instead of deriving
`Default`, and a new method `Tabs::titles()` is added to set the titles
of the tabs.

Fixes: <https://github.com/ratatui/ratatui/pull/1412>

BREAKING CHANGE: `Tabs::select()` now accepts `Into<Option<usize>>`
which breaks any code already using parameter type inference:

```diff
let selected = 1u8;
- let tabs = Tabs::new(["A", "B"]).select(selected.into())
+ let tabs = Tabs::new(["A", "B"]).select(selected as usize)
```
2024-10-14 02:44:58 -07:00
du-ob
3a43274881 feat(color): add hsluv support (#1333)
closes #763

---------

Co-authored-by: Orhun Parmaksız <orhun@archlinux.org>
2024-10-13 14:08:20 +03:00
Tayfun Bocek
dc8d0587ec feat(table)!: add support for selecting column and cell (#1331)
Fixes https://github.com/ratatui-org/ratatui/issues/1250

Adds support for selecting a column and cell in `TableState`. The
selected column, and cells style can be set by
`Table::column_highlight_style` and `Table::cell_highlight_style`
respectively.

The table example has also been updated to display the new
functionality:


https://github.com/user-attachments/assets/e5fd2858-4931-4ce1-a2f6-a5ea1eacbecc

BREAKING CHANGE: The Serialized output of the state will now include the
"selected_column" field. Software that manually parse the serialized the
output (with anything other than the `Serialize` implementation on
`TableState`) may have to be refactored if the "selected_column" field
is not accounted for. This does not affect users who rely on the
`Deserialize`, or `Serialize` implementation on the state.

BREAKING CHANGE: The `Table::highlight_style` is now deprecated in favor
of `Table::row_highlight_style`.

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-10-13 14:06:29 +03:00
Josh McKinney
23c0d52c29 feat(text): improve concise debug view for Span,Line,Text,Style (#1410)
Improves https://github.com/ratatui/ratatui/pull/1383

The following now round trips when formatted for debug.
This will make it easier to use insta when testing text related views of
widgets.

```rust
Text::from_iter([
    Line::from("Hello, world!"),
    Line::from("How are you?").bold().left_aligned(),
    Line::from_iter([
        Span::from("I'm "),
        Span::from("doing ").italic(),
        Span::from("great!").bold(),
    ]),
]).on_blue().italic().centered()
```
2024-10-08 09:08:21 +03:00
Tayfun Bocek
c32baa7cd8 chore: add benchmark for Table (#1408) 2024-10-07 12:32:05 -07:00
Raffaele Cataldo
1153a9ebaf refactor: Consistent result expected in layout tests (#1406)
Fixes #1399 
I've looked through all the `assert_eq` and made sure that they follow
the `expected, result` pattern. I wasn't sure if it was desired to
actually pass result and expected as variables to the assert_eq
statements, so I've left everything that seems to have followed the
pattern as is.
2024-10-07 00:09:42 -04:00
Josh McKinney
2805dddf05 feat(logo): Add a Ratatui logo widget
This is a simple logo widget that can be used to render the Ratatui logo
in the terminal. It is used in the `examples/ratatui-logo.rs` example,
and may be used in your applications' help or about screens.

```rust
use ratatui::{Frame, widgets::RatatuiLogo};

fn draw(frame: &mut Frame) {
    frame.render_widget(RatatuiLogo::tiny(), frame.area());
}
```
2024-10-05 17:35:43 -07:00
dependabot[bot]
baf047f556 chore(deps): update rstest requirement from 0.22.0 to 0.23.0 (#1394)
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.23.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Upgrade to async-std v1.13 by <a
href="https://github.com/jayvdb"><code>@​jayvdb</code></a> in <a
href="https://redirect.github.com/la10736/rstest/pull/274">la10736/rstest#274</a></li>
<li>Allow environment variables in #[files] attributes by <a
href="https://github.com/hansl"><code>@​hansl</code></a> in <a
href="https://redirect.github.com/la10736/rstest/pull/277">la10736/rstest#277</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/jayvdb"><code>@​jayvdb</code></a> made
their first contribution in <a
href="https://redirect.github.com/la10736/rstest/pull/274">la10736/rstest#274</a></li>
<li><a href="https://github.com/hansl"><code>@​hansl</code></a> made
their first contribution in <a
href="https://redirect.github.com/la10736/rstest/pull/277">la10736/rstest#277</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/la10736/rstest/compare/v0.22.0...v0.23.0">https://github.com/la10736/rstest/compare/v0.22.0...v0.23.0</a></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.23.0] 2024/9/29</h2>
<h3>Add</h3>
<ul>
<li>You can now use environment variables in <code>#[files]</code> with
an optional default value (see <a
href="https://redirect.github.com/la10736/rstest/pull/277">#277</a>).</li>
<li>You can now set a base_dir for <code>#[files]</code> with the
<code>$[base_dir = &quot;...&quot;]</code> attribute (see <a
href="https://redirect.github.com/la10736/rstest/pull/277">#277</a>).</li>
</ul>
<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>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="da11d4e48a"><code>da11d4e</code></a>
Update dependency and prepare the release</li>
<li><a
href="0c6e203b53"><code>0c6e203</code></a>
Update checkout list</li>
<li><a
href="20e8858611"><code>20e8858</code></a>
Make clippy happy</li>
<li><a
href="57a93425a5"><code>57a9342</code></a>
Playground should use dev package</li>
<li><a
href="8a04803580"><code>8a04803</code></a>
Removed the useless build rerun variable</li>
<li><a
href="8c232bce88"><code>8c232bc</code></a>
Add a test for invalid base_dir value</li>
<li><a
href="3520861ca5"><code>3520861</code></a>
Add tests for replace_env_vars</li>
<li><a
href="465a4012af"><code>465a401</code></a>
Add unit tests for parsing attributes, and add base_dir</li>
<li><a
href="a970613f43"><code>a970613</code></a>
Add a test for declared environment variables</li>
<li><a
href="c65d1c002c"><code>c65d1c0</code></a>
Add tests and #[ignore_missing_env_vars]</li>
<li>Additional commits viewable in <a
href="https://github.com/la10736/rstest/compare/v0.22.0...v0.23.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-10-05 23:49:25 +03:00
dependabot[bot]
6745a10508 chore(deps): update octocrab requirement from 0.40.0 to 0.41.0 (#1393)
Updates the requirements on
[octocrab](https://github.com/XAMPPRocky/octocrab) to permit the latest
version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/XAMPPRocky/octocrab/releases">octocrab's
releases</a>.</em></p>
<blockquote>
<h2>v0.41.0</h2>
<h3>Added</h3>
<ul>
<li>Implement getting users and reopos by their respective IDs (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/690">#690</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li><em>(installation)</em> [<strong>breaking</strong>] Return Result
instead of panicking in <code>Octocrab::installation</code> (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/687">#687</a>)</li>
</ul>
<h3>Other</h3>
<ul>
<li>Update tower-http requirement from 0.5.1 to 0.6.1 (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/701">#701</a>)</li>
<li>add additional webhook model fields (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/696">#696</a>)</li>
<li>Bump hyper-rustls version. (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/699">#699</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/XAMPPRocky/octocrab/blob/main/CHANGELOG.md">octocrab's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/XAMPPRocky/octocrab/compare/v0.40.0...v0.41.0">0.41.0</a>
- 2024-09-30</h2>
<h3>Added</h3>
<ul>
<li>Implement getting users and reopos by their respective IDs (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/690">#690</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li><em>(installation)</em> [<strong>breaking</strong>] Return Result
instead of panicking in <code>Octocrab::installation</code> (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/687">#687</a>)</li>
</ul>
<h3>Other</h3>
<ul>
<li>Update tower-http requirement from 0.5.1 to 0.6.1 (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/701">#701</a>)</li>
<li>add additional webhook model fields (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/696">#696</a>)</li>
<li>Bump hyper-rustls version. (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/699">#699</a>)</li>
</ul>
<h2><a
href="https://github.com/XAMPPRocky/octocrab/compare/v0.39.0...v0.40.0">0.40.0</a>
- 2024-09-22</h2>
<h3>Added</h3>
<ul>
<li>Support <code>remove_assignees</code> on issue API (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/686">#686</a>)</li>
<li>add missing fields in <code>CreateForkBuilder</code> (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/682">#682</a>)</li>
<li>Add <code>Gist::public</code> field (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/678">#678</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li><em>(refs)</em> [<strong>breaking</strong>] remove
<code>Reference::Commit</code> variant (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/697">#697</a>)</li>
</ul>
<h3>Other</h3>
<ul>
<li>Fix typo in cfg_attr statement (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/693">#693</a>)</li>
<li>Handle empty author object in pr_commits (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/656">#656</a>)</li>
<li>Add <code>DeviceCodes::poll_until_available</code> method (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/679">#679</a>)</li>
<li>Uncomment pr_commits function (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/680">#680</a>)</li>
<li>Only add base_path if req_pandq does not contain it (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/684">#684</a>)</li>
<li>Update code scanning alert (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/673">#673</a>)</li>
<li>Added <code>merged_by</code> and <code>closed_by</code> fields (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/674">#674</a>)</li>
<li>Update and Fixes to the Code Scanning Models &amp; Webhooks (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/675">#675</a>)</li>
</ul>
<h2><a
href="https://github.com/XAMPPRocky/octocrab/compare/v0.38.0...v0.39.0">0.39.0</a>
- 2024-07-30</h2>
<h3>Added</h3>
<ul>
<li>support permission on list_collaborators (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/630">#630</a>)</li>
<li>add check run pull requests and list parameters (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/494">#494</a>)</li>
<li>implement hook deliveries (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/668">#668</a>)</li>
<li>allow sending non String payload with execute (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/665">#665</a>)</li>
<li>added /user/blocks functionality (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/657">#657</a>)</li>
<li>add method to create repo webhook (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/640">#640</a>)</li>
</ul>
<h3>Fixed</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="8f7929f120"><code>8f7929f</code></a>
chore: release (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/700">#700</a>)</li>
<li><a
href="4164ea9ec6"><code>4164ea9</code></a>
fix(installation)!: Return Result instead of panicking in
`Octocrab::installa...</li>
<li><a
href="6ca41409d5"><code>6ca4140</code></a>
feat: Implement getting users and reopos by their respective IDs (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/690">#690</a>)</li>
<li><a
href="5f1ee03a9b"><code>5f1ee03</code></a>
Update tower-http requirement from 0.5.1 to 0.6.1 (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/701">#701</a>)</li>
<li><a
href="5ffd5313ee"><code>5ffd531</code></a>
add additional webhook model fields (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/696">#696</a>)</li>
<li><a
href="53e3d4f403"><code>53e3d4f</code></a>
Bump hyper-rustls version. (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/699">#699</a>)</li>
<li>See full diff in <a
href="https://github.com/XAMPPRocky/octocrab/compare/v0.40.0...v0.41.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-10-05 23:47:42 +03:00
dependabot[bot]
7799f4ff5b chore(deps): bump bnjbvr/cargo-machete from 0.6.2 to 0.7.0 (#1392)
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete)
from 0.6.2 to 0.7.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/bnjbvr/cargo-machete/releases">bnjbvr/cargo-machete's
releases</a>.</em></p>
<blockquote>
<h2>v0.7.0</h2>
<ul>
<li>Breaking change: Don't search in ignored files (those specified in
.ignore/.gitignore) by default. It's possible to use
<code>--no-ignore</code> to search in these directories by default (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/137">#137</a>).</li>
<li>Improved: fix false positives for multi dependencies single use
statements (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/120">#120</a>).
This improves precision at the cost of a small performance hit.</li>
<li>Improved: make usage of <code>--with-metadata</code> more accurate
(<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/122">#122</a>,
<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/132">#132</a>).</li>
<li>Improved: instead of displaying <code>.</code> for the current
directory, <code>cargo-machete</code> will now display <code>this
directory</code> (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/109">#109</a>).</li>
<li>Added: There's now an automated docker image build that publishes to
the <a
href="https://github.com/bnjbvr/cargo-machete/pkgs/container/cargo-machete">github
repository</a> (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/121">#121</a>).</li>
<li>Added: <code>--ignore</code> flag which make cargo-machete respect
.ignore and .gitignore files when searching for files (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/95">#95</a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/bnjbvr/cargo-machete/blob/main/CHANGELOG.md">bnjbvr/cargo-machete's
changelog</a>.</em></p>
<blockquote>
<h1>0.7.0 (released on 2024-09-25)</h1>
<ul>
<li>Breaking change: Don't search in ignored files (those specified in
.ignore/.gitignore) by default. It's possible to use
<code>--no-ignore</code> to search in these directories by default (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/137">#137</a>).</li>
<li>Improved: fix false positives for multi dependencies single use
statements (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/120">#120</a>).
This improves precision at the cost of a small performance hit.</li>
<li>Improved: make usage of <code>--with-metadata</code> more accurate
(<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/122">#122</a>,
<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/132">#132</a>).</li>
<li>Improved: instead of displaying <code>.</code> for the current
directory, <code>cargo-machete</code> will now display <code>this
directory</code> (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/109">#109</a>).</li>
<li>Added: There's now an automated docker image build that publishes to
the <a
href="https://github.com/bnjbvr/cargo-machete/pkgs/container/cargo-machete">github
repository</a> (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/121">#121</a>).</li>
<li>Added: <code>--ignore</code> flag which make cargo-machete respect
.ignore and .gitignore files when searching for files (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/95">#95</a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="5eaad10acf"><code>5eaad10</code></a>
ci: update OS versions in release task</li>
<li><a
href="a881c7261d"><code>a881c72</code></a>
Update CHANGELOG.md: address typo (thanks <a
href="https://github.com/signez"><code>@​signez</code></a>)</li>
<li><a
href="37beb07f34"><code>37beb07</code></a>
Release 0.7.0</li>
<li><a
href="ad0fea4b0b"><code>ad0fea4</code></a>
bump dependencies</li>
<li><a
href="426119c8f0"><code>426119c</code></a>
Fix typo.</li>
<li><a
href="a14e71ad3d"><code>a14e71a</code></a>
Don't search in ignored files by default</li>
<li><a
href="a767940ee9"><code>a767940</code></a>
Fix false positive for multi dep single use statements (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/120">#120</a>)</li>
<li><a
href="16e3e931d0"><code>16e3e93</code></a>
move regex-try to its own repository</li>
<li><a
href="bea9e7361a"><code>bea9e73</code></a>
nump dependencies</li>
<li><a
href="94a21474d2"><code>94a2147</code></a>
remove unnecessary clones, and some minor changes</li>
<li>Additional commits viewable in <a
href="https://github.com/bnjbvr/cargo-machete/compare/v0.6.2...v0.7.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=bnjbvr/cargo-machete&package-manager=github_actions&previous-version=0.6.2&new-version=0.7.0)](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-10-05 23:43:00 +03:00
Dheepak Krishnamurthy
edcdc8a814 refactor(layout): rename element to segment in layout (#1397)
This PR renames `element` to `segment` in a couple of functions in the
layout calculations for clarity. `element` can refer to `segment`s or
`spacer`s and functions that take only `segment`s should use `segment`
as the variable names.
2024-10-02 22:34:07 +03:00
Josh McKinney
5ad623c29b chore: remove usage of prelude (#1390)
This helps make the doc examples more explicit about what is being used.
It will also makes it a bit easier to do future refactoring of Ratatui,
into several crates, as the ambiguity of where types are coming from
will be reduced.

Additionally, several doc examples have been simplified to use Stylize,
and necessary imports are no longer hidden.

This doesn't remove the prelude. Only the internal usages.
2024-09-28 11:47:45 -07:00
Josh McKinney
bc10af5931 chore(style): make Debug output for Text/Line/Span/Style more concise (#1383)
Given:

```rust
Text::from_iter([
    Line::from("without line fields"),
    Line::from("with line fields").bold().centered(),
    Line::from_iter([
        Span::from("without span fields"),
        Span::from("with span fields")
            .green()
            .on_black()
            .italic()
            .not_dim(),
    ]),
])
```

Debug:
```
Text [Line [Span("without line fields")], Line { style: Style::new().add_modifier(Modifier::BOLD), alignment: Some(Center), spans: [Span("with line fields")] }, Line [Span("without span fields"), Span { style: Style::new().green().on_black().add_modifier(Modifier::ITALIC).remove_modifier(Modifier::DIM), content: "with span fields" }]]
```

Fixes: https://github.com/ratatui/ratatui/issues/1382
---------

Co-authored-by: Orhun Parmaksız <orhun@archlinux.org>
Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-09-26 20:38:23 +03:00
dependabot[bot]
784f67a912 chore(deps): update octocrab requirement from 0.39.0 to 0.40.0 (#1386)
Updates the requirements on
[octocrab](https://github.com/XAMPPRocky/octocrab) to permit the latest
version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/XAMPPRocky/octocrab/releases">octocrab's
releases</a>.</em></p>
<blockquote>
<h2>v0.40.0</h2>
<h3>Added</h3>
<ul>
<li>Support <code>remove_assignees</code> on issue API (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/686">#686</a>)</li>
<li>add missing fields in <code>CreateForkBuilder</code> (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/682">#682</a>)</li>
<li>Add <code>Gist::public</code> field (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/678">#678</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li><em>(refs)</em> [<strong>breaking</strong>] remove
<code>Reference::Commit</code> variant (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/697">#697</a>)</li>
</ul>
<h3>Other</h3>
<ul>
<li>Fix typo in cfg_attr statement (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/693">#693</a>)</li>
<li>Handle empty author object in pr_commits (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/656">#656</a>)</li>
<li>Add <code>DeviceCodes::poll_until_available</code> method (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/679">#679</a>)</li>
<li>Uncomment pr_commits function (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/680">#680</a>)</li>
<li>Only add base_path if req_pandq does not contain it (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/684">#684</a>)</li>
<li>Update code scanning alert (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/673">#673</a>)</li>
<li>Added <code>merged_by</code> and <code>closed_by</code> fields (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/674">#674</a>)</li>
<li>Update and Fixes to the Code Scanning Models &amp; Webhooks (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/675">#675</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/XAMPPRocky/octocrab/blob/main/CHANGELOG.md">octocrab's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/XAMPPRocky/octocrab/compare/v0.39.0...v0.40.0">0.40.0</a>
- 2024-09-22</h2>
<h3>Added</h3>
<ul>
<li>Support <code>remove_assignees</code> on issue API (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/686">#686</a>)</li>
<li>add missing fields in <code>CreateForkBuilder</code> (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/682">#682</a>)</li>
<li>Add <code>Gist::public</code> field (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/678">#678</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li><em>(refs)</em> [<strong>breaking</strong>] remove
<code>Reference::Commit</code> variant (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/697">#697</a>)</li>
</ul>
<h3>Other</h3>
<ul>
<li>Fix typo in cfg_attr statement (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/693">#693</a>)</li>
<li>Handle empty author object in pr_commits (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/656">#656</a>)</li>
<li>Add <code>DeviceCodes::poll_until_available</code> method (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/679">#679</a>)</li>
<li>Uncomment pr_commits function (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/680">#680</a>)</li>
<li>Only add base_path if req_pandq does not contain it (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/684">#684</a>)</li>
<li>Update code scanning alert (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/673">#673</a>)</li>
<li>Added <code>merged_by</code> and <code>closed_by</code> fields (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/674">#674</a>)</li>
<li>Update and Fixes to the Code Scanning Models &amp; Webhooks (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/675">#675</a>)</li>
</ul>
<h2><a
href="https://github.com/XAMPPRocky/octocrab/compare/v0.38.0...v0.39.0">0.39.0</a>
- 2024-07-30</h2>
<h3>Added</h3>
<ul>
<li>support permission on list_collaborators (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/630">#630</a>)</li>
<li>add check run pull requests and list parameters (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/494">#494</a>)</li>
<li>implement hook deliveries (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/668">#668</a>)</li>
<li>allow sending non String payload with execute (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/665">#665</a>)</li>
<li>added /user/blocks functionality (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/657">#657</a>)</li>
<li>add method to create repo webhook (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/640">#640</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>use put instead of get for set_thread_subscription (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/661">#661</a>)</li>
<li><em>(builder)</em> Change add_retry_config signature to match others
in OctocrabBuilder (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/643">#643</a>)</li>
</ul>
<h3>Other</h3>
<ul>
<li>getting Code Scanning (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/669">#669</a>)</li>
<li>added missing /repos/{owner}/{repo}/pulls/... handlers (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/546">#546</a>)
(<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/605">#605</a>)</li>
<li>Properly mark feature-gated functionality in docs (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/662">#662</a>)</li>
<li>repos/releases improvements (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/666">#666</a>)</li>
<li>Add AutoRebaseEnabled to models.rs (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/660">#660</a>)</li>
<li>cargo fmt (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/658">#658</a>)</li>
<li>Fix issue <a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/635">#635</a>
(<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/637">#637</a>)</li>
<li>Update issues.rs (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/634">#634</a>)</li>
<li>Add head repo to create pr (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/636">#636</a>)</li>
<li>Added support for make_latest in UpdateReleaseBuilder (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/646">#646</a>)</li>
<li>Changing the user name from required to optional parameter (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/650">#650</a>)</li>
<li>Update models.rs (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/651">#651</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="bd8b648282"><code>bd8b648</code></a>
chore: release (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/685">#685</a>)</li>
<li><a
href="a8bcee4c29"><code>a8bcee4</code></a>
fix(refs)!: remove <code>Reference::Commit</code> variant (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/697">#697</a>)</li>
<li><a
href="3167540eb0"><code>3167540</code></a>
Fix typo in cfg_attr statement (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/693">#693</a>)</li>
<li><a
href="01767f30eb"><code>01767f3</code></a>
Handle empty author object in pr_commits (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/656">#656</a>)</li>
<li><a
href="689ee43ab0"><code>689ee43</code></a>
feat: Support <code>remove_assignees</code> on issue API (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/686">#686</a>)</li>
<li><a
href="8aa113e079"><code>8aa113e</code></a>
Add <code>DeviceCodes::poll_until_available</code> method (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/679">#679</a>)</li>
<li><a
href="501ef7439d"><code>501ef74</code></a>
feat: add missing fields in <code>CreateForkBuilder</code> (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/682">#682</a>)</li>
<li><a
href="807cc7592d"><code>807cc75</code></a>
Uncomment pr_commits function (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/680">#680</a>)</li>
<li><a
href="0459e31986"><code>0459e31</code></a>
Only add base_path if req_pandq does not contain it (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/684">#684</a>)</li>
<li><a
href="70e9b9e49f"><code>70e9b9e</code></a>
feat: Add <code>Gist::public</code> field (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/678">#678</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/XAMPPRocky/octocrab/compare/v0.39.0...v0.40.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-09-23 21:21:17 +03:00
Josh McKinney
f4880b40cc chore: lock unicode-width version to 0.1.13 (#1384)
0.1.14 contains breaking changes which we'll need to investigate.
This commit puts a lock on the current version for now.

Changes
https://github.com/unicode-rs/unicode-width/compare/v0.1.13...v0.1.14
2024-09-21 02:58:29 -07:00
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
261 changed files with 22012 additions and 11768 deletions

2
.cargo/config.toml Normal file
View File

@@ -0,0 +1,2 @@
[alias]
xtask = "run --package xtask --"

9
.github/CODEOWNERS vendored
View File

@@ -1,8 +1,11 @@
# See https://help.github.com/articles/about-codeowners/
# 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
# <https://git-scm.com/docs/gitignore#_pattern_format>
# Maintainers
* @ratatui-org/maintainers
* @ratatui/maintainers

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
github: ratatui
open_collective: ratatui

View File

@@ -18,43 +18,22 @@ jobs:
PR_EVENT: event.json
steps:
- name: Download Benchmark Results
uses: actions/github-script@v7
uses: dawidd6/action-download-artifact@v6
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: ${{ env.BENCHMARK_RESULTS }}
run_id: ${{ github.event.workflow_run.id }}
- name: Download PR Event
uses: dawidd6/action-download-artifact@v6
with:
name: ${{ env.PR_EVENT }}
run_id: ${{ github.event.workflow_run.id }}
- 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_HEAD", prEvent.pull_request.head.ref);
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);
@@ -64,12 +43,14 @@ jobs:
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 }}' \
--branch "$PR_HEAD" \
--start-point "$PR_BASE" \
--start-point-hash "$PR_BASE_SHA" \
--start-point-clone-thresholds \
--start-point-reset \
--testbed ubuntu-latest \
--adapter rust_criterion \
--err \
--github-actions '${{ secrets.GITHUB_TOKEN }}' \
--ci-number '${{ env.PR_NUMBER }}' \
--file "$BENCHMARK_RESULTS"
--ci-number "$PR_NUMBER" \
--file "$BENCHMARK_RESULTS"

View File

@@ -9,7 +9,6 @@ on:
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
@@ -17,82 +16,92 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
env:
# don't install husky hooks during CI as they are only needed for for pre-push
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
# lint, clippy and coveraget 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.
# 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:
rustfmt:
# Lint the formatting of the codebase.
lint-formatting:
name: Check Formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- run: cargo +nightly fmt --all --check
- uses: dtolnay/rust-toolchain@nightly
with: { components: rustfmt }
- run: cargo xtask lint-formatting
typos:
# Check for typos in the codebase.
# See <https://github.com/crate-ci/typos/>
lint-typos:
name: Check Typos
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: crate-ci/typos@master
# Check for any disallowed dependencies in the codebase due to license / security issues.
# See <https://github.com/EmbarkStudios/cargo-deny>
dependencies:
name: Check Dependencies
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
clippy:
# Check for any unused dependencies in the codebase.
# See <https://github.com/bnjbvr/cargo-machete/>
cargo-machete:
name: Check Unused Dependencies
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- uses: Swatinem/rust-cache@v2
- run: cargo make clippy
- uses: bnjbvr/cargo-machete@v0.7.0
markdownlint:
# Run cargo clippy.
lint-clippy:
name: Check Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Lint markdown
uses: DavidAnson/markdownlint-cli2-action@v16
- uses: dtolnay/rust-toolchain@stable
with: { components: clippy }
- uses: Swatinem/rust-cache@v2
- run: cargo xtask lint-clippy
# Run markdownlint on all markdown files in the repository.
lint-markdown:
name: Check Markdown
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: DavidAnson/markdownlint-cli2-action@v18
with:
globs: |
'**/*.md'
'!target'
# Run cargo coverage. This will generate a coverage report and upload it to codecov.
# <https://app.codecov.io/gh/ratatui/ratatui>
coverage:
name: Coverage Report
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools
- name: Install cargo-llvm-cov and cargo-make
uses: taiki-e/install-action@v2
with:
tool: cargo-llvm-cov,cargo-make
- uses: taiki-e/install-action@cargo-llvm-cov
- uses: Swatinem/rust-cache@v2
- name: Generate coverage
run: cargo make coverage
- name: Upload to codecov.io
uses: codecov/codecov-action@v4
- run: cargo xtask coverage
- uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
# Run cargo check. This is a fast way to catch any obvious errors in the code.
check:
name: Check ${{ matrix.os }} ${{ matrix.toolchain }}
strategy:
fail-fast: false
matrix:
@@ -101,69 +110,76 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Install Rust {{ matrix.toolchain }}
uses: dtolnay/rust-toolchain@master
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- uses: Swatinem/rust-cache@v2
- run: cargo make check
env:
RUST_BACKTRACE: full
- run: cargo xtask check
lint-docs:
# Check if README.md is up-to-date with the crate's documentation.
check-readme:
name: Check README
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- uses: Swatinem/rust-cache@v2
- run: cargo make lint-docs
- uses: taiki-e/install-action@cargo-rdme
- run: cargo xtask check-readme
test-doc:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
# 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
lint-docs:
name: Check Docs
runs-on: ubuntu-latest
env:
RUSTDOCFLAGS: -Dwarnings
steps:
- uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- uses: dtolnay/rust-toolchain@nightly
- uses: dtolnay/install@cargo-docs-rs
- uses: Swatinem/rust-cache@v2
- name: Test docs
run: cargo make test-doc
env:
RUST_BACKTRACE: full
- run: cargo xtask lint-docs
test:
# Run cargo test on the documentation of the crate. This will catch any code examples that don't
# compile, or any other issues in the documentation.
test-docs:
name: Test Docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo xtask test-docs
# Run cargo test on the libraries of the crate.
test-libs:
name: Test Libs ${{ matrix.toolchain }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
toolchain: ["1.74.0", "stable"]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo xtask test-libs
# Run cargo test on all the backends.
test-backends:
name: Test ${{matrix.backend}} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
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
- name: Install Rust ${{ matrix.toolchain }}}
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Install cargo-nextest
uses: taiki-e/install-action@nextest
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo make test-backend ${{ matrix.backend }}
env:
RUST_BACKTRACE: full
- run: cargo xtask test-backend ${{ matrix.backend }}

View File

@@ -1,4 +1,4 @@
name: Continuous Deployment
name: Release alpha version
on:
workflow_dispatch:
@@ -6,9 +6,6 @@ on:
# At 00:00 on Saturday
# https://crontab.guru/#0_0_*_*_6
- cron: "0 0 * * 6"
push:
tags:
- "v*.*.*"
defaults:
run:
@@ -20,7 +17,6 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
if: ${{ !startsWith(github.event.ref, 'refs/tags/v') }}
steps:
- name: Checkout the repository
uses: actions/checkout@v4
@@ -30,14 +26,14 @@ jobs:
- name: Calculate the next release
run: .github/workflows/calculate-alpha-release.bash
- name: Publish on crates.io
uses: actions-rs/cargo@v1
with:
command: publish
args: --allow-dirty --token ${{ secrets.CARGO_TOKEN }}
- 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@v3
uses: orhun/git-cliff-action@v4
with:
config: cliff.toml
args: --unreleased --tag ${{ env.NEXT_TAG }} --strip header
@@ -50,17 +46,3 @@ jobs:
tag: ${{ env.NEXT_TAG }}
prerelease: true
bodyFile: BODY.md
publish-stable:
name: Create a stable release
runs-on: ubuntu-latest
if: ${{ startsWith(github.event.ref, 'refs/tags/v') }}
steps:
- name: Checkout the repository
uses: actions/checkout@v4
- name: Publish on crates.io
uses: actions-rs/cargo@v1
with:
command: publish
args: --token ${{ secrets.CARGO_TOKEN }}

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 }}

1
.gitignore vendored
View File

@@ -1,5 +1,4 @@
target
Cargo.lock
*.log
*.rs.rustfmt
.gdb_history

View File

@@ -4,14 +4,23 @@ This document contains a list of breaking changes in each version and some notes
between versions. It is compiled manually from the commit history and changelog. We also tag PRs on
GitHub with a [breaking change] label.
[breaking change]: (https://github.com/ratatui-org/ratatui/issues?q=label%3A%22breaking+change%22)
[breaking change]: (https://github.com/ratatui/ratatui/issues?q=label%3A%22breaking+change%22)
## Summary
This is a quick summary of the sections below:
- [Unreleased](#unreleased)
- The `From` impls for backend types are now replaced with more specific traits
- [v0.29.0](#v0290)
- `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const
- Removed public fields from `Rect` iterators
- `Line` now implements `From<Cow<str>`
- `Table::highlight_style` is now `Table::row_highlight_style`
- `Tabs::select` now accepts `Into<Option<usize>>`
- `Color::from_hsl` is now behind the `palette` feature
- [v0.28.0](#v0280)
`Backend::size` returns `Size` instead of `Rect`
- `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>>`
@@ -65,18 +74,200 @@ This is a quick summary of the sections below:
- MSRV is now 1.63.0
- `List` no longer ignores empty strings
## v0.28.0
## Unreleased (0.30.0)
### `WidgetRef` no longer has a blanket implementation of Widget
Previously there was a blanket implementation of Widget for WidgetRef. This has been reversed to
instead be a blanket implementation of WidgetRef for all &W where W: Widget. Any widgets that
previously implemented WidgetRef directly should now instead implement Widget for a reference to the
type.
```diff
-impl WidgetRef for Foo {
- fn render_ref(&self, area: Rect, buf: &mut Buffer)
+impl Widget for &Foo {
+ fn render(self, area: Rect, buf: &mut Buffer)
}
```
### The `From` impls for backend types are now replaced with more specific traits [#1464]
[#1464]: https://github.com/ratatui/ratatui/pull/1464
Crossterm gains `ratatui::backend::crossterm::{FromCrossterm, IntoCrossterm}`
Termwiz gains `ratatui::backend::termwiz::{FromTermwiz, IntoTermwiz}`
This is necessary in order to avoid the orphan rule when implementing `From` for crossterm types
once the crossterm types are moved to a separate crate.
```diff
+ use ratatui::backend::crossterm::{FromCrossterm, IntoCrossterm};
let crossterm_color = crossterm::style::Color::Black;
- let ratatui_color = crossterm_color.into();
- let ratatui_color = ratatui::style::Color::from(crossterm_color);
+ let ratatui_color = ratatui::style::Color::from_crossterm(crossterm_color);
- let crossterm_color = ratatui_color.into();
- let crossterm_color = crossterm::style::Color::from(ratatui_color);
+ let crossterm_color = ratatui_color.into_crossterm();
let crossterm_attribute = crossterm::style::types::Attribute::Bold;
- let ratatui_modifier = crossterm_attribute.into();
- let ratatui_modifier = ratatui::style::Modifier::from(crossterm_attribute);
+ let ratatui_modifier = ratatui::style::Modifier::from_crossterm(crossterm_attribute);
- let crossterm_attribute = ratatui_modifier.into();
- let crossterm_attribute = crossterm::style::types::Attribute::from(ratatui_modifier);
+ let crossterm_attribute = ratatui_modifier.into_crossterm();
```
Similar conversions for `ContentStyle` -> `Style` and `Attributes` -> `Modifier` exist for
Crossterm and the various Termion and Termwiz types as well.
### `Bar::label()` and `BarGroup::label()` now accepts `Into<Line<'a>>`. ([#1471])
[#1471]: https://github.com/ratatui/ratatui/pull/1471
Previously `Bar::label()` and `BarGroup::label()` accepted `Line<'a>`, but they now accepts `Into<Line<'a>>`.
for `Bar::label()`:
```diff
- Bar::default().label("foo".into());
+ Bar::default().label("foo");
```
for `BarGroup::label()`:
```diff
- BarGroup::default().label("bar".into());
+ BarGroup::default().label("bar");
```
### `Bar::text_value` now accepts `Into<String>` ([#1471])
Previously `Bar::text_value` accepted `String`, but now it accepts `Into<String>`.
for `Bar::text_value()`:
```diff
- Bar::default().text_value("foobar".into());
+ Bar::default().text_value("foobar");
```
## [v0.29.0](https://github.com/ratatui/ratatui/releases/tag/v0.29.0)
### `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const ([#1326])
[#1326]: https://github.com/ratatui/ratatui/pull/1326
The `Sparkline::data` method has been modified to accept `IntoIterator<Item = SparklineBar>`
instead of `&[u64]`.
`SparklineBar` is a struct that contains an `Option<u64>` value, which represents an possible
_absent_ value, as distinct from a `0` value. This change allows the `Sparkline` to style
data points differently, depending on whether they are present or absent.
`SparklineBar` also contains an `Option<Style>` that will be used to apply a style the bar in
addition to any other styling applied to the `Sparkline`.
Several `From` implementations have been added to `SparklineBar` to support existing callers who
provide `&[u64]` and other types that can be converted to `SparklineBar`, such as `Option<u64>`.
If you encounter any type inference issues, you may need to provide an explicit type for the data
passed to `Sparkline::data`. For example, if you are passing a single value, you may need to use
`into()` to convert it to form that can be used as a `SparklineBar`:
```diff
let value = 1u8;
- Sparkline::default().data(&[value.into()]);
+ Sparkline::default().data(&[u64::from(value)]);
```
As a consequence of this change, the `data` method is no longer a `const fn`.
### `Color::from_hsl` is now behind the `palette` feature and accepts `palette::Hsl` ([#1418])
[#1418]: https://github.com/ratatui/ratatui/pull/1418
Previously `Color::from_hsl` accepted components as individual f64 parameters. It now accepts a
single `palette::Hsl` value and is gated behind a `palette` feature flag.
```diff
- Color::from_hsl(360.0, 100.0, 100.0)
+ Color::from_hsl(Hsl::new(360.0, 100.0, 100.0))
```
### Removed public fields from `Rect` iterators ([#1358], [#1424])
[#1358]: https://github.com/ratatui/ratatui/pull/1358
[#1424]: https://github.com/ratatui/ratatui/pull/1424
The `pub` modifier has been removed from fields on the `Columns`,`Rows`, and `Positions` iterators.
These fields were not intended to be public and should not have been accessed directly.
### `Rect::area()` now returns u32 instead of u16 ([#1378])
[#1378]: https://github.com/ratatui/ratatui/pull/1378
This is likely to impact anything which relies on `Rect::area` maxing out at u16::MAX. It can now
return up to u16::MAX * u16::MAX (2^32 - 2^17 + 1).
### `Line` now implements `From<Cow<str>` ([#1373])
[#1373]: https://github.com/ratatui/ratatui/pull/1373
As this adds an extra conversion, ambiguous inferred expressions may no longer compile.
```rust
// given:
struct Foo { ... }
impl From<Foo> for String { ... }
impl From<Foo> for Cow<str> { ... }
let foo = Foo { ... };
let line = Line::from(foo); // now fails due to now ambiguous inferred type
// replace with e.g.
let line = Line::from(String::from(foo));
```
### `Tabs::select()` now accepts `Into<Option<usize>>` ([#1413])
[#1413]: https://github.com/ratatui/ratatui/pull/1413
Previously `Tabs::select()` accepted `usize`, but it now accepts `Into<Option<usize>>`. This breaks
any code already using parameter type inference:
```diff
let selected = 1u8;
- let tabs = Tabs::new(["A", "B"]).select(selected.into())
+ let tabs = Tabs::new(["A", "B"]).select(selected as usize)
```
### `Table::highlight_style` is now `Table::row_highlight_style` ([#1331])
[#1331]: https://github.com/ratatui/ratatui/pull/1331
The `Table::highlight_style` is now deprecated in favor of `Table::row_highlight_style`.
Also, the serialized output of the `TableState` will now include the "selected_column" field.
Software that manually parse the serialized the output (with anything other than the `Serialize`
implementation on `TableState`) may have to be refactored if the "selected_column" field is not
accounted for. This does not affect users who rely on the `Deserialize`, or `Serialize`
implementation on the state.
## [v0.28.0](https://github.com/ratatui/ratatui/releases/tag/v0.28.0)
### `Backend::size` returns `Size` instead of `Rect` ([#1254])
[#1254]: https://github.com/ratatui-org/ratatui/pull/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-org/ratatui/pull/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.
@@ -87,7 +278,7 @@ and a default implementation for them exists.
### Ratatui now requires Crossterm 0.28.0 ([#1278])
[#1278]: https://github.com/ratatui-org/ratatui/pull/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
@@ -95,8 +286,8 @@ version (0.27.0). Ratatui re-exports the version of crossterm that it is compati
### `Axis::labels()` now accepts `IntoIterator<Into<Line>>` ([#1273] and [#1283])
[#1273]: https://github.com/ratatui-org/ratatui/pull/1173
[#1283]: https://github.com/ratatui-org/ratatui/pull/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.
@@ -108,7 +299,7 @@ type will need to be rewritten as the compiler cannot infer the correct type.
### `Layout::init_cache` no longer returns bool and takes a `NonZeroUsize` instead of `usize` ([#1245])
[#1245]: https://github.com/ratatui-org/ratatui/pull/1245
[#1245]: https://github.com/ratatui/ratatui/pull/1245
```diff
- let is_initialized = Layout::init_cache(100);
@@ -117,7 +308,7 @@ type will need to be rewritten as the compiler cannot infer the correct type.
### `ratatui::terminal` module is now private ([#1160])
[#1160]: https://github.com/ratatui-org/ratatui/pull/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
@@ -130,21 +321,21 @@ are also named terminal, and confusion about module exports for newer Rust users
### `ToText` no longer has a lifetime ([#1234])
[#1234]: https://github.com/ratatui-org/ratatui/pull/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`
### `Frame::size` is deprecated and renamed to `Frame::area` ([#1293])
[#1293]: https://github.com/ratatui-org/ratatui/pull/1293
[#1293]: https://github.com/ratatui/ratatui/pull/1293
`Frame::size` is renamed to `Frame::area` as its the more correct name.
`Frame::size` is renamed to `Frame::area` as it's the more correct name.
## [v0.27.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.27.0)
## [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-org/ratatui/pull/1149
[#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`.
@@ -205,11 +396,11 @@ A change is only necessary if you were matching on all variants of the `MouseEve
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-org/ratatui/pull/1106
[#1106]: https://github.com/ratatui/ratatui/pull/1106
### `Rect::inner` takes `Margin` directly instead of reference ([#1008])
[#1008]: https://github.com/ratatui-org/ratatui/pull/1008
[#1008]: https://github.com/ratatui/ratatui/pull/1008
`Margin` needs to be passed without reference now.
@@ -223,7 +414,7 @@ wildcard. In this case, you need to either handle the two new variants, `MouseLe
### `Buffer::filled` takes `Cell` directly instead of reference ([#1148])
[#1148]: https://github.com/ratatui-org/ratatui/pull/1148
[#1148]: https://github.com/ratatui/ratatui/pull/1148
`Buffer::filled` moves the `Cell` instead of taking a reference.
@@ -234,14 +425,14 @@ wildcard. In this case, you need to either handle the two new variants, `MouseLe
### `Stylize::bg()` now accepts `Into<Color>` ([#1103])
[#1103]: https://github.com/ratatui-org/ratatui/pull/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-org/ratatui/pull/759
[#759]: https://github.com/ratatui/ratatui/pull/759
`List::start_corner` was deprecated in v0.25. Use `List::direction` and `ListDirection` instead.
@@ -264,7 +455,7 @@ flexible types from calling scopes, though it can break some type inference in t
### `LineGauge::gauge_style` is deprecated ([#565])
[#565]: https://github.com/ratatui-org/ratatui/pull/1148
[#565]: https://github.com/ratatui/ratatui/pull/1148
`LineGauge::gauge_style` is deprecated and replaced with `LineGauge::filled_style` and `LineGauge::unfilled_style`:
@@ -275,11 +466,11 @@ let gauge = LineGauge::default()
+ .unfilled_style(Style::default().fg(Color::White));
```
## [v0.26.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.26.0)
## [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-org/ratatui/pull/881
[#881]: https://github.com/ratatui/ratatui/pull/881
Previously, constraints would stretch to fill all available space, violating constraints if
necessary.
@@ -300,7 +491,7 @@ existing layouts with `Flex::Start`. However, to get old behavior, use `Flex::Le
### `Table::new()` now accepts `IntoIterator<Item: Into<Row<'a>>>` ([#774])
[#774]: https://github.com/ratatui-org/ratatui/pull/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
@@ -317,7 +508,7 @@ This can be resolved either by providing an explicit type (e.g. `Vec::<Row>::new
### `Tabs::new()` now accepts `IntoIterator<Item: Into<Line<'a>>>` ([#776])
[#776]: https://github.com/ratatui-org/ratatui/pull/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.
@@ -333,7 +524,7 @@ by removing the call to `.collect()`.
### Table::default() now sets segment_size to None and column_spacing to ([#751])
[#751]: https://github.com/ratatui-org/ratatui/pull/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.
@@ -343,7 +534,7 @@ To use the previous default values, call `table.segment_size(Default::default())
### `patch_style` & `reset_style` now consumes and returns `Self` ([#754])
[#754]: https://github.com/ratatui-org/ratatui/pull/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
@@ -369,7 +560,7 @@ The following example shows how to migrate for `Line`, but the same applies for
### `Block` style methods cannot be used in a const context ([#720])
[#720]: https://github.com/ratatui-org/ratatui/pull/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
@@ -377,7 +568,7 @@ longer can be called from a constant context.
### `Line` now has a `style` field that applies to the entire line ([#708])
[#708]: https://github.com/ratatui-org/ratatui/pull/708
[#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
@@ -401,11 +592,11 @@ the `Span::style` field.
.alignment(Alignment::Left);
```
## [v0.25.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.25.0)
## [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-org/ratatui/pull/691
[#691]: https://github.com/ratatui/ratatui/pull/691
These items were deprecated since 0.10.
@@ -419,7 +610,7 @@ These items were deprecated since 0.10.
### `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>` ([#672])
[#672]: https://github.com/ratatui-org/ratatui/pull/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).
@@ -434,7 +625,7 @@ E.g.
### The default `Tabs::highlight_style` is now `Style::new().reversed()` ([#635])
[#635]: https://github.com/ratatui-org/ratatui/pull/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.
@@ -446,7 +637,7 @@ widget in the default configuration would not show any indication of the selecte
### `Table::new()` now requires specifying the widths of the columns ([#664])
[#664]: https://github.com/ratatui-org/ratatui/pull/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:
@@ -472,7 +663,7 @@ or complex, it may be convenient to replace `Table::new` with `Table::default().
### `Table::widths()` now accepts `IntoIterator<Item = AsRef<Constraint>>` ([#663])
[#663]: https://github.com/ratatui-org/ratatui/pull/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
@@ -488,7 +679,7 @@ E.g.
### Layout::new() now accepts direction and constraint parameters ([#557])
[#557]: https://github.com/ratatui-org/ratatui/pull/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.
@@ -505,18 +696,18 @@ let layout = layout::default()
let layout = layout::new(Direction::Vertical, [Constraint::Min(1), Constraint::Max(2)]);
```
## [v0.24.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.24.0)
## [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-org/ratatui/pull/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-org/ratatui/issues/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
@@ -530,7 +721,7 @@ Applications can now set custom borders on a `Block` by calling `border_set()`.
### Generic `Backend` parameter removed from `Frame` ([#530])
[#530]: https://github.com/ratatui-org/ratatui/issues/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
@@ -544,7 +735,7 @@ instance of a Frame. E.g.:
### `Stylize` shorthands now consume rather than borrow `String` ([#466])
[#466]: https://github.com/ratatui-org/ratatui/issues/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
@@ -562,7 +753,7 @@ longer compile. E.g.
### Deprecated `Spans` type removed (replaced with `Line`) ([#426])
[#426]: https://github.com/ratatui-org/ratatui/issues/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`.
@@ -575,11 +766,11 @@ longer compile. E.g.
+ buffer.set_line(0, 0, line, 10);
```
## [v0.23.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.23.0)
## [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-org/ratatui/issues/360
[#360]: https://github.com/ratatui/ratatui/issues/360
The track symbol of `Scrollbar` is now optional, this method now takes an optional value.
@@ -591,7 +782,7 @@ The track symbol of `Scrollbar` is now optional, this method now takes an option
### `Scrollbar` symbols moved to `symbols::scrollbar` and `widgets::scrollbar` module is private ([#330])
[#330]: https://github.com/ratatui-org/ratatui/issues/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
@@ -605,31 +796,31 @@ new module locations. E.g.:
### MSRV updated to 1.67 ([#361])
[#361]: https://github.com/ratatui-org/ratatui/issues/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-org/ratatui/releases/tag/v0.22.0)
## [v0.22.0](https://github.com/ratatui/ratatui/releases/tag/v0.22.0)
### `bitflags` updated to 2.3 ([#205])
[#205]: https://github.com/ratatui-org/ratatui/issues/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-org/ratatui/releases/tag/v0.21.0)
## [v0.21.0](https://github.com/ratatui/ratatui/releases/tag/v0.21.0)
### MSRV is 1.65.0 ([#171])
[#171]: https://github.com/ratatui-org/ratatui/issues/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-org/ratatui/issues/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.
@@ -646,7 +837,7 @@ let terminal = Terminal::with_options(backend, TerminalOptions {
### Code that binds `Into<Text<'a>>` now requires type annotations ([#168])
[#168]: https://github.com/ratatui-org/ratatui/issues/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
@@ -660,7 +851,7 @@ previously did not need to use type annotations to fail to compile. To fix this,
### `Marker::Block` now renders as a block rather than a bar character ([#133])
[#133]: https://github.com/ratatui-org/ratatui/issues/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
@@ -672,20 +863,20 @@ the existing code.
+ let canvas = Canvas::default().marker(Marker::Bar);
```
## [v0.20.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.20.0)
## [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-org/ratatui/issues/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-org/ratatui/issues/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.

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ creating a new issue before making the change, or starting a discussion on
## Reporting issues
Before reporting an issue on the [issue tracker](https://github.com/ratatui-org/ratatui/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.
@@ -31,7 +31,7 @@ guarantee that the behavior is unchanged.
### Code formatting
Run `cargo make format` before committing to ensure that code is consistently formatted with
Run `cargo xtask 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
@@ -56,7 +56,7 @@ 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.
Running `cargo xtask 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.
@@ -71,22 +71,22 @@ in GitHub docs.
### Setup
Clone the repo and build it using [cargo-make](https://sagiegurari.github.io/cargo-make/)
TL;DR: Clone the repo and build it using `cargo xtask`.
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
[cargo-xtask](https://github.com/matklad/cargo-xtask). 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`.
`cargo xtask build`.
```shell
git clone https://github.com/ratatui-org/ratatui.git
git clone https://github.com/ratatui/ratatui.git
cd ratatui
cargo make build
cargo xtask build
```
### Tests
The [test coverage](https://app.codecov.io/gh/ratatui-org/ratatui) of the crate is reasonably
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
@@ -171,7 +171,7 @@ time to update. However, if a deprecation is blocking for us to implement a new
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-org/ratatui/discussions/66) for more about the decision.
discussion](https://github.com/ratatui/ratatui/discussions/66) for more about the decision.
## Continuous Integration
@@ -182,7 +182,7 @@ We use GitHub Actions for the CI where we perform the following checks:
- The code should conform to the default format enforced by `rustfmt`.
- The code should not contain common style issues `clippy`.
You can also check most of those things yourself locally using `cargo make ci` which will offer you
You can also check most of those things yourself locally using `cargo xtask ci` which will offer you
a shorter feedback loop than pushing to github.
## Relationship with `tui-rs`
@@ -196,7 +196,7 @@ it is useful to refer to when contributing code, documentation, or issues with R
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-org/ratatui/pulls?q=is%3Apr+is%3Aopen+label%3A%22imported+from+tui%22).
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.

3953
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,369 +1,55 @@
[package]
name = "ratatui"
version = "0.28.0" # crate version
[workspace]
resolver = "2"
members = ["ratatui", "ratatui-*", "xtask"]
default-members = [
"ratatui",
"ratatui-core",
"ratatui-crossterm",
# this is not included as it doesn't compile on windows
# "ratatui-termion",
"ratatui-termwiz",
"ratatui-widgets",
]
[workspace.package]
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-org/ratatui"
repository = "https://github.com/ratatui/ratatui"
homepage = "https://ratatui.rs"
keywords = ["tui", "terminal", "dashboard"]
categories = ["command-line-interface"]
readme = "README.md"
license = "MIT"
exclude = [
"assets/*",
".github",
"Makefile.toml",
"CONTRIBUTING.md",
"*.log",
"tags",
]
exclude = ["assets/*", ".github", "Makefile.toml", "CONTRIBUTING.md", "*.log", "tags"]
edition = "2021"
rust-version = "1.74.0"
[badges]
[dependencies]
bitflags = "2.3"
cassowary = "0.3"
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", features = ["derive"] }
strum_macros = { version = "0.26.3" }
termion = { version = "4.0.0", optional = true }
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"
[dev-dependencies]
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"] }
derive_builder = "0.20.0"
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"]
## 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-org/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 = []
[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"]
[workspace.dependencies]
bitflags = "2.6.0"
color-eyre = "0.6.3"
crossterm = "0.28.1"
document-features = "0.2.7"
indoc = "2.0.5"
instability = "0.3.3"
itertools = "0.13.0"
pretty_assertions = "1.4.1"
ratatui = { path = "ratatui", version = "0.30.0-alpha.0" }
ratatui-core = { path = "ratatui-core", version = "0.1.0-alpha.0" }
ratatui-crossterm = { path = "ratatui-crossterm", version = "0.1.0-alpha.0" }
ratatui-termion = { path = "ratatui-termion", version = "0.1.0-alpha.0" }
ratatui-termwiz = { path = "ratatui-termwiz", version = "0.1.0-alpha.0" }
ratatui-widgets = { path = "ratatui-widgets", version = "0.3.0-alpha.0" }
rstest = "0.23.0"
serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.133"
strum = { version = "0.26.3", features = ["derive"] }
termwiz = { version = "0.22.0" }
unicode-segmentation = "1.12.0"
# See <https://github.com/ratatui/ratatui/issues/1271> for information about why we pin unicode-width
unicode-width = "=0.2.0"
termion = "4.0.0"
# Improve benchmark consistency
[profile.bench]
codegen-units = 1
lto = true
[lib]
bench = false
[[bench]]
name = "main"
harness = false
[[example]]
name = "async"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "barchart"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "barchart-grouped"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "block"
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", "unstable-widget-ref"]
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"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
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
[[test]]
name = "state_serde"
required-features = ["serde"]

View File

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

View File

@@ -1,180 +0,0 @@
# configuration for https://github.com/sagiegurari/cargo-make
[config]
skip_core_tasks = true
[env]
# all features except the backend ones
ALL_FEATURES = "all-widgets,macros,serde"
[env.ALL_FEATURES_FLAG]
# Windows does not support building termion, so this avoids the build failure by providing two
# sets of flags, one for Windows and one for other platforms.
source = "${CARGO_MAKE_RUST_TARGET_OS}"
default_value = "--features=all-widgets,macros,serde,crossterm,termion,termwiz,underline-color,unstable"
mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz,underline-color,unstable" }
[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", "lint-markdown"]
[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",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
"--",
"-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",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
]
[tasks.build]
description = "Compile the project"
command = "cargo"
args = [
"build",
"--all-targets",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
]
[tasks.clippy]
description = "Run Clippy for linting"
command = "cargo"
args = [
"clippy",
"--all-targets",
"--tests",
"--benches",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
"--",
"-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",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
]
[tasks.test-doc]
description = "Run documentation tests"
command = "cargo"
args = ["test", "--doc", "--no-default-features", "${ALL_FEATURES_FLAG}"]
[tasks.test-backend]
# takes a command line parameter to specify the backend to test (e.g. "crossterm")
description = "Run backend-specific tests"
dependencies = ["install-nextest"]
command = "cargo"
args = [
"nextest",
"run",
"--all-targets",
"--no-default-features",
"--features",
"${ALL_FEATURES},${@}",
]
[tasks.coverage]
description = "Generate code coverage report"
command = "cargo"
args = [
"llvm-cov",
"--lcov",
"--output-path",
"target/lcov.info",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
]
[tasks.run-example]
private = true
condition = { env_set = ["TUI_EXAMPLE_NAME"] }
command = "cargo"
args = [
"run",
"--release",
"--example",
"${TUI_EXAMPLE_NAME}",
"--features",
"all-widgets",
]
[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
'''

363
README.md
View File

@@ -2,16 +2,19 @@
<summary>Table of Contents</summary>
- [Ratatui](#ratatui)
- [Installation](#installation)
- [Quick Start](#quickstart)
- [Other documentation](#other-documentation)
- [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)
- [Layout](#layout)
- [Text and styling](#text-and-styling)
- [Status of this fork](#status-of-this-fork)
- [Rust version requirements](#rust-version-requirements)
- [Widgets](#widgets)
- [Built in](#built-in)
- [Third\-party libraries, bootstrapping templates and
widgets](#third-party-libraries-bootstrapping-templates-and-widgets)
- [Third-party libraries, bootstrapping templates and widgets](#third-party-libraries-bootstrapping-templates-and-widgets)
- [Apps](#apps)
- [Alternatives](#alternatives)
- [Acknowledgments](#acknowledgments)
@@ -21,7 +24,7 @@
<!-- cargo-rdme start -->
![Demo](https://github.com/ratatui-org/ratatui/blob/87ae72dbc756067c97f6400d3e2a58eeb383776e/examples/demo2-destroy.gif?raw=true)
![Demo](https://github.com/ratatui/ratatui/blob/87ae72dbc756067c97f6400d3e2a58eeb383776e/examples/demo2-destroy.gif?raw=true)
<div align="center">
@@ -41,28 +44,42 @@ Badge]][GitHub Sponsors]<br> [![Discord Badge]][Discord Server] [![Matrix Badge]
lightweight library that provides a set of widgets and utilities to build complex Rust TUIs.
Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its development.
## Installation
## Quickstart
Add `ratatui` as a dependency to your cargo.toml:
Add `ratatui` and `crossterm` as dependencies to your cargo.toml:
```shell
cargo add ratatui
cargo add ratatui crossterm
```
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]).
Then you can create a simple "Hello World" application:
## Introduction
```rust
use crossterm::event::{self, Event};
use ratatui::{text::Text, Frame};
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.
fn main() {
let mut terminal = ratatui::init();
loop {
terminal.draw(draw).expect("failed to draw frame");
if matches!(event::read().expect("failed to read event"), Event::Key(_)) {
break;
}
}
ratatui::restore();
}
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.
fn draw(frame: &mut Frame) {
let text = Text::raw("Hello World!");
frame.render_widget(text, frame.area());
}
```
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.
## Other documentation
@@ -74,46 +91,82 @@ terminal user interfaces and showcases the features of Ratatui, along with a hel
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
- [Breaking Changes] - a list of breaking changes in the library.
## Quickstart
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.
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.
## Introduction
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.
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]).
Every application built with `ratatui` needs to implement the following steps:
- Initialize the terminal
- A main loop to:
- Handle input events
- Draw the UI
- A main loop that:
- Draws the UI
- Handles input events
- 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].
The [`Terminal`] type is the main entry point for any Ratatui application. It is generic over a
a choice of [`Backend`] implementations that each provide functionality to draw frames, clear
the screen, hide the cursor, etc. There are backend 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.
The simplest way to initialize the terminal is to use the [`init`] function which returns a
[`DefaultTerminal`] instance with the default options, enters the Alternate Screen and Raw mode
and sets up a panic hook that restores the terminal in case of panic. This instance can then be
used to draw frames and interact with the terminal state. (The [`DefaultTerminal`] instance is a
type alias for a terminal with the [`crossterm`] backend.) The [`restore`] function restores the
terminal to its original state.
```rust
fn main() -> std::io::Result<()> {
let mut terminal = ratatui::init();
let result = run(&mut terminal);
ratatui::restore();
result
}
```
See the [`backend` module] and the [Backends] section of the [Ratatui Website] for more info on
the alternate screen and raw mode.
### 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.
Drawing the UI is done by calling the [`Terminal::draw`] method on the terminal instance. This
method takes a closure that is called with a [`Frame`] instance. 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.
The closure passed to the [`Terminal::draw`] method should handle the rendering of a full frame.
```rust
use ratatui::{widgets::Paragraph, Frame};
fn run(terminal: &mut ratatui::DefaultTerminal) -> std::io::Result<()> {
loop {
terminal.draw(|frame| draw(frame))?;
if handle_events()? {
break Ok(());
}
}
}
fn draw(frame: &mut Frame) {
let text = Paragraph::new("Hello World!");
frame.render_widget(text, frame.area());
}
```
### Handling events
@@ -122,63 +175,23 @@ calling backend library methods directly. See the [Handling Events] section of t
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 crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
use ratatui::{
backend::CrosstermBackend,
crossterm::{
event::{self, Event, KeyCode},
terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
fn handle_events() -> std::io::Result<bool> {
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
KeyCode::Char('q') => return Ok(true),
// handle other key events
_ => {}
},
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);
}
}
// handle other events
_ => {}
}
Ok(false)
}
fn ui(frame: &mut Frame) {
frame.render_widget(
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
frame.size(),
);
}
```
Running this example produces the following output:
![docsrs-hello]
## Layout
The library comes with a basic yet useful layout management object called [`Layout`] which
@@ -193,16 +206,13 @@ use ratatui::{
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.size());
let [left_area, right_area] =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.areas(main_area);
fn draw(frame: &mut Frame) {
use Constraint::{Fill, Length, Min};
let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
let [title_area, main_area, status_area] = vertical.areas(frame.area());
let horizontal = Layout::horizontal([Fill(1); 2]);
let [left_area, right_area] = horizontal.areas(main_area);
frame.render_widget(Block::bordered().title("Title Bar"), title_area);
frame.render_widget(Block::bordered().title("Status Bar"), status_area);
@@ -213,7 +223,13 @@ fn ui(frame: &mut Frame) {
Running this example produces the following output:
![docsrs-layout]
```text
Title Bar───────────────────────────────────
┌Left────────────────┐┌Right───────────────┐
│ ││ │
└────────────────────┘└────────────────────┘
Status Bar──────────────────────────────────
```
## Text and styling
@@ -236,8 +252,8 @@ use ratatui::{
Frame,
};
fn ui(frame: &mut Frame) {
let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.size());
fn draw(frame: &mut Frame) {
let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.area());
let line = Line::from(vec![
Span::raw("Hello "),
@@ -266,10 +282,6 @@ fn ui(frame: &mut Frame) {
}
```
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/
@@ -280,21 +292,18 @@ Running this example produces the following output:
[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-org/templates/
[Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples/README.md
[Report a bug]: https://github.com/ratatui-org/ratatui/issues/new?labels=bug&projects=&template=bug_report.md
[Request a Feature]: https://github.com/ratatui-org/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md
[Create a Pull Request]: https://github.com/ratatui-org/ratatui/compare
[templates]: https://github.com/ratatui/templates/
[Examples]: https://github.com/ratatui/ratatui/tree/main/ratatui/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-org/ratatui/blob/main/CHANGELOG.md
[Contributing]: https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md
[Breaking Changes]: https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md
[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-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
[docsrs-layout]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
[docsrs-styling]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
[`Frame`]: terminal::Frame
[`render_widget`]: terminal::Frame::render_widget
[`Widget`]: widgets::Widget
@@ -313,15 +322,15 @@ Running this example produces the following output:
[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-org
[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-org/ratatui/ci.yml?style=flat-square&logo=github
[CI Workflow]: https://github.com/ratatui-org/ratatui/actions/workflows/ci.yml
[Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
[Codecov]: https://app.codecov.io/gh/ratatui-org/ratatui
[Deps.rs Badge]: https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square
[Deps.rs]: https://deps.rs/repo/github/ratatui-org/ratatui
[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
@@ -329,100 +338,42 @@ Running this example produces the following output:
[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-org?logo=github&style=flat-square&color=1370D3
[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 🚀
## Contributing
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].
We have also recently launched the [Ratatui Forum][Forum], 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.
Please make sure you read the [contributing](./CONTRIBUTING.md) guidelines, especially if you are
interested in working on a PR or issue opened in the previous repository.
## Widgets
## Built with Ratatui
### 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-org/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-org/awesome-ratatui) for a curated list of
awesome apps/libraries built with `ratatui`!
Ratatui has a number of built-in [widgets](https://docs.rs/ratatui/latest/ratatui/widgets/), as well
as many contributed by external contributors. Check out the [Showcase](https://ratatui.rs/showcase/)
section of the website, or the [awesome-ratatui](https://github.com/ratatui/awesome-ratatui) repo
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
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) or
[iocraft](https://github.com/ccbrown/iocraft/) for an alternative solutions
to build text user interfaces in Rust.
## Acknowledgments
None of this could be possible without [**Florian Dehau**](https://github.com/fdehau) who originally
created [tui-rs] which inspired many Rust TUIs.
Special thanks to [**Pavel Fomchenkov**](https://github.com/nawok) for his work in designing **an
awesome logo** for the ratatui project and ratatui-org organization.
awesome logo** for the ratatui project and ratatui organization.
## License

View File

@@ -1,5 +1,12 @@
# 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.
@@ -12,7 +19,7 @@ actions](.github/workflows/cd.yml) and triggered by pushing a tag.
```
1. Switch branches to the images branch and copy demo2.gif to examples/, commit, and push.
1. Grab the permalink from <https://github.com/ratatui-org/ratatui/blob/images/examples/demo2.gif> and
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.
@@ -21,16 +28,16 @@ actions](.github/workflows/cd.yml) and triggered by pushing a tag.
can be used for generating the entries.
1. Ensure that any breaking changes are documented in [BREAKING-CHANGES.md](./BREAKING-CHANGES.md)
1. Commit and push the changes.
1. Create a new tag: `git tag -a v[X.Y.Z]`
1. 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-org/ratatui/actions) workflow to
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-org/ratatui/actions/workflows/cd.yml) workflow.
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
@@ -40,5 +47,5 @@ These releases will have whatever happened to be in main at the time of release,
for apps that need to get releases from crates.io, but may contain more bugs and be generally less
tested than normal releases.
See [#147](https://github.com/ratatui-org/ratatui/issues/147) and
[#359](https://github.com/ratatui-org/ratatui/pull/359) for more info on the alpha release process.
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.

View File

@@ -6,4 +6,4 @@ We only support the latest version of this crate.
## Reporting a Vulnerability
To report secuirity vulnerability, please use the form at <https://github.com/ratatui-org/ratatui/security/advisories/new>
To report secuirity vulnerability, please use the form at <https://github.com/ratatui/ratatui/security/advisories/new>

View File

@@ -8,58 +8,66 @@
default_job = "check"
[jobs.check]
command = ["cargo", "check", "--all-features", "--color", "always"]
command = ["cargo", "check", "--all-features"]
need_stdout = false
[jobs.check-all]
command = ["cargo", "check", "--all-targets", "--all-features", "--color", "always"]
command = ["cargo", "check", "--all-targets", "--all-features"]
need_stdout = false
[jobs.check-crossterm]
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "crossterm"]
command = [
"cargo",
"check",
"--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"]
command = [
"cargo",
"check",
"--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"]
command = [
"cargo",
"check",
"--all-targets",
"--no-default-features",
"--features",
"termwiz",
]
need_stdout = false
[jobs.clippy]
command = [
"cargo", "clippy",
"--all-targets",
"--color", "always",
]
command = ["cargo", "clippy", "--all-targets"]
need_stdout = false
[jobs.test]
command = [
"cargo", "test",
"--all-features",
"--color", "always",
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
]
command = ["cargo", "test", "--all-features"]
need_stdout = true
[jobs.test-unit]
command = [
"cargo", "test",
"--lib",
"--all-features",
"--color", "always",
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
]
command = ["cargo", "test", "--lib", "--all-features"]
need_stdout = true
[jobs.doc]
command = [
"cargo", "+nightly", "doc",
"-Zunstable-options", "-Zrustdoc-scrape-examples",
"cargo",
"+nightly",
"doc",
"-Zunstable-options",
"-Zrustdoc-scrape-examples",
"--all-features",
"--color", "always",
"--no-deps",
]
env.RUSTDOCFLAGS = "--cfg docsrs"
@@ -69,10 +77,12 @@ need_stdout = false
# to the previous job
[jobs.doc-open]
command = [
"cargo", "+nightly", "doc",
"-Zunstable-options", "-Zrustdoc-scrape-examples",
"cargo",
"+nightly",
"doc",
"-Zunstable-options",
"-Zrustdoc-scrape-examples",
"--all-features",
"--color", "always",
"--no-deps",
"--open",
]
@@ -82,19 +92,34 @@ 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",
"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",
"cargo",
"llvm-cov",
"--lcov",
"--output-path",
"target/lcov.info",
"--lib",
"--all-features",
"--color", "always",
]
[jobs.hack]
command = [
"cargo",
"hack",
"test",
"--lib",
"--each-feature",
# "--all-targets",
"--workspace",
]
# You may define here keybindings that would be specific to
@@ -102,7 +127,7 @@ command = [
# Shortcuts to internal functions (scrolling, toggling, etc.)
# should go in your personal global prefs.toml file instead.
[keybindings]
# alt-m = "job:my-job"
ctrl-h = "job:hack"
ctrl-c = "job:check-crossterm"
ctrl-t = "job:check-termion"
ctrl-w = "job:check-termwiz"

View File

@@ -1,24 +0,0 @@
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);

View File

@@ -2,7 +2,7 @@
# https://git-cliff.org/docs/configuration
[remote.github]
owner = "ratatui-org"
owner = "ratatui"
repo = "ratatui"
[changelog]
@@ -24,25 +24,15 @@ body = """
{%- if not version %}
## [unreleased]
{% else -%}
## [{{ version }}](https://github.com/ratatui-org/ratatui/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
## [{{ 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-org/ratatui/commit/" ~ commit.id }}) \
- [{{ 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.remote.username %} by @{{ commit.remote.username }}{%- endif -%}\
{% if commit.remote.pr_number %} in [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.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") %}

View File

@@ -3,9 +3,15 @@ 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-org/ratatui/pull/1064#issuecomment-2078848980
# (also, memoffset, syn, nix, strsim, windows-sys
# crossterm -> all the windows- deps https://github.com/ratatui/ratatui/pull/1064#issuecomment-2078848980
allowed-duplicate-crates = [
"bitflags",
"memoffset",
"nix",
"strsim",
"syn",
"windows-sys",
"windows-targets",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
@@ -14,4 +20,5 @@ allowed-duplicate-crates = [
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"unicode-width",
]

View File

@@ -11,6 +11,7 @@ allow = [
"MIT",
"Unicode-DFS-2016",
"WTFPL",
"Zlib",
]
[advisories]

View File

@@ -1,199 +0,0 @@
//! # [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-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use color_eyre::Result;
use rand::{thread_rng, Rng};
use ratatui::{
crossterm::event::{self, Event, KeyCode},
layout::{Constraint, Direction, Layout},
prelude::{Color, Line, Style, Stylize},
widgets::{Bar, BarChart, BarGroup, Block},
};
use self::terminal::Terminal;
struct App {
should_exit: bool,
temperatures: Vec<u8>,
}
fn main() -> Result<()> {
color_eyre::install()?;
let mut terminal = terminal::init()?;
let app = App::new();
app.run(&mut terminal)?;
terminal::restore()?;
Ok(())
}
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 run(mut self, terminal: &mut Terminal) -> Result<()> {
while !self.should_exit {
self.draw(terminal)?;
self.handle_events()?;
}
Ok(())
}
fn draw(&self, terminal: &mut Terminal) -> Result<()> {
terminal.draw(|frame| self.render(frame))?;
Ok(())
}
fn handle_events(&mut self) -> Result<()> {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
self.should_exit = true;
}
}
Ok(())
}
fn render(&self, frame: &mut ratatui::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()
.map(|v| u64::from(*v))
.enumerate()
.map(|(i, value)| {
Bar::default()
.value(value)
.label(Line::from(format!("{i:>02}:00")))
.text_value(format!("{value:>3}°"))
.style(temperature_style(value))
.value_style(temperature_style(value).reversed())
})
.collect();
let title = Line::from("Weather (Vertical)").centered();
BarChart::default()
.data(BarGroup::default().bars(&bars))
.block(Block::new().title(title))
.bar_width(5)
}
/// Create a horizontal bar chart from the temperatures data.
fn horizontal_barchart(temperatures: &[u8]) -> BarChart {
let bars: Vec<Bar> = temperatures
.iter()
.map(|v| u64::from(*v))
.enumerate()
.map(|(i, value)| {
let style = temperature_style(value);
Bar::default()
.value(value)
.label(Line::from(format!("{i:>02}:00")))
.text_value(format!("{value:>3}°"))
.style(style)
.value_style(style.reversed())
})
.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)
}
/// create a yellow to red value based on the value (50-90)
fn temperature_style(value: u64) -> Style {
let green = (255.0 * (1.0 - (value - 50) as f64 / 40.0)) as u8;
let color = Color::Rgb(255, green, 0);
Style::new().fg(color)
}
/// Contains functions common to all examples
mod terminal {
use std::{
io::{self, stdout, Stdout},
panic,
};
use ratatui::{
backend::CrosstermBackend,
crossterm::{
execute,
terminal::{
disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen,
LeaveAlternateScreen,
},
},
};
// A type alias to simplify the usage of the terminal and make it easier to change the backend
// or choice of writer.
pub type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
/// Initialize the terminal by enabling raw mode and entering the alternate screen.
///
/// This function should be called before the program starts to ensure that the terminal is in
/// the correct state for the application.
pub fn init() -> io::Result<Terminal> {
install_panic_hook();
enable_raw_mode()?;
execute!(stdout(), EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
/// Restore the terminal by leaving the alternate screen and disabling raw mode.
///
/// This function should be called before the program exits to ensure that the terminal is
/// restored to its original state.
pub fn restore() -> io::Result<()> {
disable_raw_mode()?;
execute!(
stdout(),
LeaveAlternateScreen,
Clear(ClearType::FromCursorDown),
)
}
/// Install a panic hook that restores the terminal before printing the panic.
///
/// This prevents error messages from being messed up by the terminal state.
fn install_panic_hook() {
let panic_hook = panic::take_hook();
panic::set_hook(Box::new(move |panic_info| {
let _ = restore();
panic_hook(panic_info);
}));
}
}

View File

@@ -1,258 +0,0 @@
//! # [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-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{
error::Error,
io::{stdout, Stdout},
ops::ControlFlow,
time::Duration,
};
use itertools::Itertools;
use ratatui::{
backend::CrosstermBackend,
crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
layout::{Alignment, Constraint, Layout, Rect},
style::{Style, Stylize},
text::Line,
widgets::{
block::{Position, Title},
Block, BorderType, Borders, Padding, Paragraph, Wrap,
},
Frame,
};
// These type aliases are used to make the code more readable by reducing repetition of the generic
// types. They are not necessary for the functionality of the code.
type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
type Result<T> = std::result::Result<T, Box<dyn Error>>;
fn main() -> Result<()> {
let mut terminal = setup_terminal()?;
let result = run(&mut terminal);
restore_terminal(terminal)?;
if let Err(err) = result {
eprintln!("{err:?}");
}
Ok(())
}
fn setup_terminal() -> Result<Terminal> {
enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal(mut terminal: Terminal) -> Result<()> {
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
Ok(())
}
fn run(terminal: &mut Terminal) -> Result<()> {
loop {
terminal.draw(ui)?;
if handle_events()?.is_break() {
return Ok(());
}
}
}
fn handle_events() -> Result<ControlFlow<()>> {
if event::poll(Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
return Ok(ControlFlow::Break(()));
}
}
}
Ok(ControlFlow::Continue(()))
}
fn ui(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_vec();
(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);
}
// Note: this currently renders incorrectly, see https://github.com/ratatui-org/ratatui/issues/349
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(
Title::from("top left")
.position(Position::Top)
.alignment(Alignment::Left),
)
.title(
Title::from("top center")
.position(Position::Top)
.alignment(Alignment::Center),
)
.title(
Title::from("top right")
.position(Position::Top)
.alignment(Alignment::Right),
)
.title(
Title::from("bottom left")
.position(Position::Bottom)
.alignment(Alignment::Left),
)
.title(
Title::from("bottom center")
.position(Position::Bottom)
.alignment(Alignment::Center),
)
.title(
Title::from("bottom right")
.position(Position::Bottom)
.alignment(Alignment::Right),
);
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);
}

View File

@@ -1,18 +0,0 @@
use color_eyre::{config::HookBuilder, Result};
use crate::term;
pub fn init_hooks() -> Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = term::restore();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = term::restore();
panic(info);
}));
Ok(())
}

View File

@@ -1,43 +0,0 @@
//! # [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-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/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 errors;
mod tabs;
mod term;
mod theme;
use color_eyre::Result;
pub use self::{
colors::{color_from_oklab, RgbSwatch},
theme::THEME,
};
fn main() -> Result<()> {
errors::init_hooks()?;
let terminal = &mut term::init()?;
app::run(terminal)?;
term::restore()?;
Ok(())
}

View File

@@ -1,46 +0,0 @@
use std::{
io::{self, stdout},
time::Duration,
};
use color_eyre::{eyre::WrapErr, Result};
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::{
event::{self, Event},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
},
layout::Rect,
Terminal, TerminalOptions, Viewport,
};
pub fn init() -> Result<Terminal<impl Backend>> {
// this size is to match the size of the terminal when running the demo
// using vhs in a 1280x640 sized window (github social preview size)
let options = TerminalOptions {
viewport: Viewport::Fixed(Rect::new(0, 0, 81, 18)),
};
let terminal = Terminal::with_options(CrosstermBackend::new(io::stdout()), options)?;
enable_raw_mode().context("enable raw mode")?;
stdout()
.execute(EnterAlternateScreen)
.wrap_err("enter alternate screen")?;
Ok(terminal)
}
pub fn restore() -> Result<()> {
disable_raw_mode().context("disable raw mode")?;
stdout()
.execute(LeaveAlternateScreen)
.wrap_err("leave alternate screen")?;
Ok(())
}
pub fn next_event(timeout: Duration) -> Result<Option<Event>> {
if !event::poll(timeout)? {
return Ok(None);
}
let event = event::read()?;
Ok(Some(event))
}

View File

@@ -1,112 +0,0 @@
//! # [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-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{
io::{self, Stdout},
time::Duration,
};
use color_eyre::{eyre::Context, Result};
use ratatui::{
backend::CrosstermBackend,
crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
widgets::Paragraph,
Frame, Terminal,
};
/// 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 mut terminal = init_terminal().context("setup failed")?;
let result = run(&mut terminal).context("app loop failed");
restore_terminal();
result
}
/// Setup the terminal. This is where you would enable raw mode, enter the alternate screen, and
/// hide the cursor. This example does not handle errors.
fn init_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
set_panic_hook();
let mut stdout = io::stdout();
enable_raw_mode().context("failed to enable raw mode")?;
execute!(stdout, EnterAlternateScreen).context("unable to enter alternate screen")?;
Terminal::new(CrosstermBackend::new(stdout)).context("creating terminal failed")
}
/// Restore the terminal. This is where you disable raw mode, leave the alternate screen, and show
/// the cursor.
fn restore_terminal() {
// There's not a lot we can do if these fail, so we just print an error message.
if let Err(err) = disable_raw_mode() {
eprintln!("Error disabling raw mode: {err}");
}
if let Err(err) = execute!(io::stdout(), LeaveAlternateScreen) {
eprintln!("Error leaving alternate screen: {err}");
}
}
/// Replace the default panic hook with one that restores the terminal before panicking.
fn set_panic_hook() {
let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
restore_terminal();
hook(panic_info);
}));
}
/// 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(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
loop {
terminal.draw(render_app)?;
if should_quit()? {
break;
}
}
Ok(())
}
/// Render the application. This is where you would draw the application UI. This example just
/// draws a greeting.
fn render_app(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 so that the application can exit in a timely
/// manner, and 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)
}

View File

@@ -1,48 +0,0 @@
//! # [Ratatui] Minimal 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-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use ratatui::{
backend::CrosstermBackend,
crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
text::Text,
Terminal,
};
/// 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
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stdout()))?;
enable_raw_mode()?;
execute!(terminal.backend_mut(), EnterAlternateScreen)?;
loop {
terminal.draw(|frame| frame.render_widget(Text::raw("Hello World!"), frame.area()))?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
break;
}
}
}
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
Ok(())
}

View File

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

View File

@@ -1,130 +0,0 @@
//! # [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-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
// See also https://github.com/joshka/tui-popup and
// https://github.com/sephiroth74/tui-confirm-dialog
use std::{error::Error, io};
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
layout::{Constraint, Layout, Rect},
style::Stylize,
widgets::{Block, Clear, Paragraph, Wrap},
Frame, Terminal,
};
struct App {
show_popup: bool,
}
impl App {
const fn new() -> Self {
Self { show_popup: false }
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let app = App::new();
let res = run_app(&mut terminal, app);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f, &app))?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Char('p') => app.show_popup = !app.show_popup,
_ => {}
}
}
}
}
}
fn ui(f: &mut Frame, app: &App) {
let area = f.area();
let vertical = Layout::vertical([Constraint::Percentage(20), Constraint::Percentage(80)]);
let [instructions, content] = vertical.areas(area);
let text = if app.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 });
f.render_widget(paragraph, instructions);
let block = Block::bordered().title("Content").on_blue();
f.render_widget(block, content);
if app.show_popup {
let block = Block::bordered().title("Popup");
let area = centered_rect(60, 20, area);
f.render_widget(Clear, area); //this clears out the background
f.render_widget(block, area);
}
}
/// helper function to create a centered rect using up certain percentage of the available rect `r`
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::vertical([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
Layout::horizontal([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
}

View File

@@ -1,85 +0,0 @@
//! # [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-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{
io::{self, stdout},
thread::sleep,
time::Duration,
};
use indoc::indoc;
use itertools::izip;
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::terminal::{disable_raw_mode, enable_raw_mode},
widgets::Paragraph,
Terminal, 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 = init()?;
terminal.draw(|frame| frame.render_widget(Paragraph::new(logo()), frame.area()))?;
sleep(Duration::from_secs(5));
restore()?;
println!();
Ok(())
}
fn init() -> io::Result<Terminal<impl Backend>> {
enable_raw_mode()?;
let options = TerminalOptions {
viewport: Viewport::Inline(3),
};
Terminal::with_options(CrosstermBackend::new(stdout()), options)
}
fn restore() -> io::Result<()> {
disable_raw_mode()?;
Ok(())
}

View File

@@ -1,238 +0,0 @@
//! # [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-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
#![warn(clippy::pedantic)]
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
layout::{Alignment, Constraint, Layout, Margin},
style::{Color, Style, Stylize},
symbols::scrollbar,
text::{Line, Masked, Span},
widgets::{Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
Frame, Terminal,
};
#[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<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::default();
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &mut app))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Char('j') | KeyCode::Down => {
app.vertical_scroll = app.vertical_scroll.saturating_add(1);
app.vertical_scroll_state =
app.vertical_scroll_state.position(app.vertical_scroll);
}
KeyCode::Char('k') | KeyCode::Up => {
app.vertical_scroll = app.vertical_scroll.saturating_sub(1);
app.vertical_scroll_state =
app.vertical_scroll_state.position(app.vertical_scroll);
}
KeyCode::Char('h') | KeyCode::Left => {
app.horizontal_scroll = app.horizontal_scroll.saturating_sub(1);
app.horizontal_scroll_state =
app.horizontal_scroll_state.position(app.horizontal_scroll);
}
KeyCode::Char('l') | KeyCode::Right => {
app.horizontal_scroll = app.horizontal_scroll.saturating_add(1);
app.horizontal_scroll_state =
app.horizontal_scroll_state.position(app.horizontal_scroll);
}
_ => {}
}
}
}
if last_tick.elapsed() >= tick_rate {
last_tick = Instant::now();
}
}
}
#[allow(clippy::too_many_lines, clippy::cast_possible_truncation)]
fn ui(f: &mut Frame, app: &mut App) {
let area = f.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)),
]),
];
app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len());
app.horizontal_scroll_state = app.horizontal_scroll_state.content_length(long_line.len());
let create_block = |title: &'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());
f.render_widget(title, chunks[0]);
let paragraph = Paragraph::new(text.clone())
.gray()
.block(create_block("Vertical scrollbar with arrows"))
.scroll((app.vertical_scroll as u16, 0));
f.render_widget(paragraph, chunks[1]);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some(""))
.end_symbol(Some("")),
chunks[1],
&mut app.vertical_scroll_state,
);
let paragraph = Paragraph::new(text.clone())
.gray()
.block(create_block(
"Vertical scrollbar without arrows, without track symbol and mirrored",
))
.scroll((app.vertical_scroll as u16, 0));
f.render_widget(paragraph, chunks[2]);
f.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 app.vertical_scroll_state,
);
let paragraph = Paragraph::new(text.clone())
.gray()
.block(create_block(
"Horizontal scrollbar with only begin arrow & custom thumb symbol",
))
.scroll((0, app.horizontal_scroll as u16));
f.render_widget(paragraph, chunks[3]);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.thumb_symbol("🬋")
.end_symbol(None),
chunks[3].inner(Margin {
vertical: 0,
horizontal: 1,
}),
&mut app.horizontal_scroll_state,
);
let paragraph = Paragraph::new(text.clone())
.gray()
.block(create_block(
"Horizontal scrollbar without arrows & custom thumb and track symbol",
))
.scroll((0, app.horizontal_scroll as u16));
f.render_widget(paragraph, chunks[4]);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.thumb_symbol("")
.track_symbol(Some("")),
chunks[4].inner(Margin {
vertical: 0,
horizontal: 1,
}),
&mut app.horizontal_scroll_state,
);
}

View File

@@ -1,183 +0,0 @@
//! # [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-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use rand::{
distributions::{Distribution, Uniform},
rngs::ThreadRng,
};
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
layout::{Constraint, Layout},
style::{Color, Style},
widgets::{Block, Borders, Sparkline},
Frame, Terminal,
};
#[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))
}
}
struct App {
signal: RandomSignal,
data1: Vec<u64>,
data2: Vec<u64>,
data3: Vec<u64>,
}
impl 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>>();
Self {
signal,
data1,
data2,
data3,
}
}
fn on_tick(&mut self) {
let value = self.signal.next().unwrap();
self.data1.pop();
self.data1.insert(0, value);
let value = self.signal.next().unwrap();
self.data2.pop();
self.data2.insert(0, value);
let value = self.signal.next().unwrap();
self.data3.pop();
self.data3.insert(0, value);
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::new();
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &app))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui(f: &mut Frame, app: &App) {
let chunks = Layout::vertical([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Min(0),
])
.split(f.area());
let sparkline = Sparkline::default()
.block(
Block::new()
.borders(Borders::LEFT | Borders::RIGHT)
.title("Data1"),
)
.data(&app.data1)
.style(Style::default().fg(Color::Yellow));
f.render_widget(sparkline, chunks[0]);
let sparkline = Sparkline::default()
.block(
Block::new()
.borders(Borders::LEFT | Borders::RIGHT)
.title("Data2"),
)
.data(&app.data2)
.style(Style::default().bg(Color::Green));
f.render_widget(sparkline, chunks[1]);
// Multiline
let sparkline = Sparkline::default()
.block(
Block::new()
.borders(Borders::LEFT | Borders::RIGHT)
.title("Data3"),
)
.data(&app.data3)
.style(Style::default().fg(Color::Red));
f.render_widget(sparkline, chunks[2]);
}

View File

@@ -1,274 +0,0 @@
//! # [Ratatui] User Input 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-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
// A simple example demonstrating how to handle user input. This is a bit out of the scope of
// the library as it does not provide any input handling out of the box. However, it may helps
// some to get started.
//
// This is a very simple example:
// * An input box always focused. Every character you type is registered here.
// * An entered character is inserted at the cursor position.
// * Pressing Backspace erases the left character before the cursor position
// * Pressing Enter pushes the current input in the history of previous messages. **Note: ** as
// this is a relatively simple example unicode characters are unsupported and their use will
// result in undefined behaviour.
//
// See also https://github.com/rhysd/tui-textarea and https://github.com/sayanarijit/tui-input/
use std::{error::Error, io};
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
layout::{Constraint, Layout, Position},
style::{Color, Modifier, Style, Stylize},
text::{Line, Span, Text},
widgets::{Block, List, ListItem, Paragraph},
Frame, Terminal,
};
enum InputMode {
Normal,
Editing,
}
/// App holds the state of the application
struct App {
/// Current value of the input box
input: String,
/// Position of cursor in the editor area.
character_index: usize,
/// Current input mode
input_mode: InputMode,
/// History of recorded messages
messages: Vec<String>,
}
impl App {
const fn new() -> Self {
Self {
input: String::new(),
input_mode: InputMode::Normal,
messages: Vec::new(),
character_index: 0,
}
}
fn move_cursor_left(&mut self) {
let cursor_moved_left = self.character_index.saturating_sub(1);
self.character_index = self.clamp_cursor(cursor_moved_left);
}
fn move_cursor_right(&mut self) {
let cursor_moved_right = self.character_index.saturating_add(1);
self.character_index = self.clamp_cursor(cursor_moved_right);
}
fn enter_char(&mut self, new_char: char) {
let index = self.byte_index();
self.input.insert(index, new_char);
self.move_cursor_right();
}
/// Returns the byte index based on the character position.
///
/// Since each character in a string can be contain multiple bytes, it's necessary to calculate
/// the byte index based on the index of the character.
fn byte_index(&self) -> usize {
self.input
.char_indices()
.map(|(i, _)| i)
.nth(self.character_index)
.unwrap_or(self.input.len())
}
fn delete_char(&mut self) {
let is_not_cursor_leftmost = self.character_index != 0;
if is_not_cursor_leftmost {
// Method "remove" is not used on the saved text for deleting the selected char.
// Reason: Using remove on String works on bytes instead of the chars.
// Using remove would require special care because of char boundaries.
let current_index = self.character_index;
let from_left_to_current_index = current_index - 1;
// Getting all characters before the selected character.
let before_char_to_delete = self.input.chars().take(from_left_to_current_index);
// Getting all characters after selected character.
let after_char_to_delete = self.input.chars().skip(current_index);
// Put all characters together except the selected one.
// By leaving the selected one out, it is forgotten and therefore deleted.
self.input = before_char_to_delete.chain(after_char_to_delete).collect();
self.move_cursor_left();
}
}
fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
new_cursor_pos.clamp(0, self.input.chars().count())
}
fn reset_cursor(&mut self) {
self.character_index = 0;
}
fn submit_message(&mut self) {
self.messages.push(self.input.clone());
self.input.clear();
self.reset_cursor();
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let app = App::new();
let res = run_app(&mut terminal, app);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f, &app))?;
if let Event::Key(key) = event::read()? {
match app.input_mode {
InputMode::Normal => match key.code {
KeyCode::Char('e') => {
app.input_mode = InputMode::Editing;
}
KeyCode::Char('q') => {
return Ok(());
}
_ => {}
},
InputMode::Editing if key.kind == KeyEventKind::Press => match key.code {
KeyCode::Enter => app.submit_message(),
KeyCode::Char(to_insert) => {
app.enter_char(to_insert);
}
KeyCode::Backspace => {
app.delete_char();
}
KeyCode::Left => {
app.move_cursor_left();
}
KeyCode::Right => {
app.move_cursor_right();
}
KeyCode::Esc => {
app.input_mode = InputMode::Normal;
}
_ => {}
},
InputMode::Editing => {}
}
}
}
}
fn ui(f: &mut Frame, app: &App) {
let vertical = Layout::vertical([
Constraint::Length(1),
Constraint::Length(3),
Constraint::Min(1),
]);
let [help_area, input_area, messages_area] = vertical.areas(f.area());
let (msg, style) = match app.input_mode {
InputMode::Normal => (
vec![
"Press ".into(),
"q".bold(),
" to exit, ".into(),
"e".bold(),
" to start editing.".bold(),
],
Style::default().add_modifier(Modifier::RAPID_BLINK),
),
InputMode::Editing => (
vec![
"Press ".into(),
"Esc".bold(),
" to stop editing, ".into(),
"Enter".bold(),
" to record the message".into(),
],
Style::default(),
),
};
let text = Text::from(Line::from(msg)).patch_style(style);
let help_message = Paragraph::new(text);
f.render_widget(help_message, help_area);
let input = Paragraph::new(app.input.as_str())
.style(match app.input_mode {
InputMode::Normal => Style::default(),
InputMode::Editing => Style::default().fg(Color::Yellow),
})
.block(Block::bordered().title("Input"));
f.render_widget(input, input_area);
match app.input_mode {
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
InputMode::Normal => {}
// Make the cursor visible and ask ratatui to put it at the specified coordinates after
// rendering
#[allow(clippy::cast_possible_truncation)]
InputMode::Editing => f.set_cursor_position(Position::new(
// Draw the cursor at the current position in the input field.
// This position is can be controlled via the left and right arrow key
input_area.x + app.character_index as u16 + 1,
// Move one line down, from the border to the input line
input_area.y + 1,
)),
}
let messages: Vec<ListItem> = app
.messages
.iter()
.enumerate()
.map(|(i, m)| {
let content = Line::from(Span::raw(format!("{i}: {m}")));
ListItem::new(content)
})
.collect();
let messages = List::new(messages).block(Block::bordered().title("Messages"));
f.render_widget(messages, messages_area);
}

70
ratatui-core/Cargo.toml Normal file
View File

@@ -0,0 +1,70 @@
[package]
name = "ratatui-core"
description = """
Core types and traits for the Ratatui Terminal UI library.
Widget libraries should use this crate. Applications should use the main Ratatui crate.
"""
version = "0.1.0-alpha.0"
readme = "README.md"
authors.workspace = true
documentation.workspace = true
repository.workspace = true
homepage.workspace = true
keywords.workspace = true
categories.workspace = true
license.workspace = true
exclude.workspace = true
edition.workspace = true
rust-version.workspace = true
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[features]
default = []
## enables conversions from colors in the [`palette`] crate to [`Color`](crate::style::Color).
palette = ["dep:palette"]
## enables the backend code that sets the underline color. Underline color is only supported by
## the Crossterm backend, and is not supported on Windows 7.
underline-color = []
## Use terminal scrolling regions to make some operations less prone to
## flickering. (i.e. Terminal::insert_before).
scrolling-regions = []
## 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"]
[dependencies]
bitflags = "2.3"
cassowary = "0.3"
compact_str = "0.8.0"
document-features = { workspace = true, optional = true }
indoc.workspace = true
itertools.workspace = true
lru = "0.12.0"
palette = { version = "0.7.6", optional = true }
paste = "1.0.2"
serde = { workspace = true, optional = true }
strum.workspace = true
unicode-segmentation.workspace = true
unicode-truncate = "2"
unicode-width.workspace = true
[dev-dependencies]
pretty_assertions.workspace = true
ratatui = { workspace = true, features = ["crossterm", "termwiz"] }
rstest.workspace = true
serde_json.workspace = true
[target.'cfg(not(windows))'.dev-dependencies]
ratatui = { workspace = true, features = ["termion"] }
[lints.clippy]
# 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"

41
ratatui-core/README.md Normal file
View File

@@ -0,0 +1,41 @@
# Ratatui Core
[![Crates.io](https://img.shields.io/crates/v/ratatui-core)](https://crates.io/crates/ratatui-core)
[![Documentation](https://docs.rs/ratatui-core/badge.svg)](https://docs.rs/ratatui-core)
[![License](https://img.shields.io/crates/l/ratatui-core)](../LICENSE)
<!-- ⚠️ DO NOT EDIT THIS FILE DIRECTLY, EDIT lib.rs AND THEN RUN `cargo rdme` to update this file. -->
<!-- cargo-rdme start -->
**ratatui-core** is the core library of the [ratatui] project,
providing the essential building blocks for creating rich terminal user interfaces in Rust.
[ratatui]: https://github.com/ratatui/ratatui
### Why `ratatui-core`?
The `ratatui-core` crate is split from the main [`ratatui`](https://crates.io/crates/ratatui) crate
to offer better stability for widget library authors. Widget libraries should generally depend
on `ratatui-core`, benefiting from a stable API and reducing the need for frequent updates.
Applications, on the other hand, should depend on the main `ratatui` crate, which includes
built-in widgets and additional features.
## Installation
Add `ratatui-core` to your `Cargo.toml`:
```shell
cargo add ratatui-core
```
## Contributing
We welcome contributions from the community! Please see our [CONTRIBUTING](../CONTRIBUTING.md)
guide for more details on how to get involved.
### License
This project is licensed under the MIT License. See the [LICENSE](../LICENSE) file for details.
<!-- cargo-rdme end -->

View File

@@ -27,7 +27,7 @@
//! ```rust,no_run
//! use std::io::stdout;
//!
//! use ratatui::prelude::*;
//! use ratatui::{backend::CrosstermBackend, Terminal};
//!
//! let backend = CrosstermBackend::new(stdout());
//! let mut terminal = Terminal::new(backend)?;
@@ -56,7 +56,7 @@
//! Each backend handles raw mode differently, so the behavior may vary depending on the backend
//! being used. Be sure to consult the backend's specific documentation for exact details on how it
//! implements raw mode.
//!
//! # Alternate Screen
//!
//! The alternate screen is a separate buffer that some terminals provide, distinct from the main
@@ -90,16 +90,16 @@
//! backend being used, and developers should consult the specific backend's documentation to
//! understand how it implements mouse capture.
//!
//! [`TermionBackend`]: termion/struct.TermionBackend.html
//! [`Terminal`]: crate::terminal::Terminal
//! [`TermionBackend`]: termion/struct.TermionBackend.html
//! [`CrosstermBackend`]: https://docs.rs/ratatui/latest/ratatui/backend/struct.CrosstermBackend.html
//! [`TermionBackend`]: https://docs.rs/ratatui/latest/ratatui/backend/struct.TermionBackend.html
//! [`TermwizBackend`]: https://docs.rs/ratatui/latest/ratatui/backend/struct.TermwizBackend.html
//! [`Terminal`]: https://docs.rs/ratatui/latest/ratatui/struct.Terminal.html
//! [Crossterm]: https://crates.io/crates/crossterm
//! [Termion]: https://crates.io/crates/termion
//! [Termwiz]: https://crates.io/crates/termwiz
//! [Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples/README.md
//! [Backend Comparison]:
//! https://ratatui.rs/concepts/backends/comparison/
//! [Ratatui Website]: https://ratatui-org.github.io/ratatui-book
//! [Examples]: https://github.com/ratatui/ratatui/tree/main/ratatui/examples/README.md
//! [Backend Comparison]: https://ratatui.rs/concepts/backends/comparison/
//! [Ratatui Website]: https://ratatui.rs
use std::io;
use strum::{Display, EnumString};
@@ -109,21 +109,6 @@ use crate::{
layout::{Position, Size},
};
#[cfg(feature = "termion")]
mod termion;
#[cfg(feature = "termion")]
pub use self::termion::TermionBackend;
#[cfg(feature = "crossterm")]
mod crossterm;
#[cfg(feature = "crossterm")]
pub use self::crossterm::CrosstermBackend;
#[cfg(feature = "termwiz")]
mod termwiz;
#[cfg(feature = "termwiz")]
pub use self::termwiz::TermwizBackend;
mod test;
pub use self::test::TestBackend;
@@ -162,7 +147,7 @@ pub struct WindowSize {
/// Most applications should not need to interact with the `Backend` trait directly as the
/// [`Terminal`] struct provides a higher level interface for interacting with the terminal.
///
/// [`Terminal`]: crate::terminal::Terminal
/// [`Terminal`]: https://docs.rs/ratatui/latest/ratatui/struct.Terminal.html
pub trait Backend {
/// Draw the given content to the terminal screen.
///
@@ -187,8 +172,10 @@ pub trait Backend {
/// # Example
///
/// ```rust
/// # use ratatui::backend::{Backend, TestBackend};
/// # use ratatui::backend::{TestBackend};
/// # let mut backend = TestBackend::new(80, 25);
/// use ratatui::backend::Backend;
///
/// backend.hide_cursor()?;
/// // do something with hidden cursor
/// backend.show_cursor()?;
@@ -222,9 +209,10 @@ pub trait Backend {
/// # Example
///
/// ```rust
/// # use ratatui::backend::{Backend, TestBackend};
/// # use ratatui::layout::Position;
/// # use ratatui::backend::{TestBackend};
/// # let mut backend = TestBackend::new(80, 25);
/// use ratatui::{backend::Backend, layout::Position};
///
/// backend.set_cursor_position(Position { x: 10, y: 20 })?;
/// assert_eq!(backend.get_cursor_position()?, Position { x: 10, y: 20 });
/// # std::io::Result::Ok(())
@@ -254,8 +242,10 @@ pub trait Backend {
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::{Backend, TestBackend};
/// # use ratatui::backend::{TestBackend};
/// # let mut backend = TestBackend::new(80, 25);
/// use ratatui::backend::Backend;
///
/// backend.clear()?;
/// # std::io::Result::Ok(())
/// ```
@@ -270,8 +260,10 @@ pub trait Backend {
/// # Example
///
/// ```rust,no_run
/// # use ratatui::{prelude::*, backend::{TestBackend, ClearType}};
/// # use ratatui::{backend::{TestBackend}};
/// # let mut backend = TestBackend::new(80, 25);
/// use ratatui::backend::{Backend, ClearType};
///
/// backend.clear_region(ClearType::All)?;
/// # std::io::Result::Ok(())
/// ```
@@ -302,8 +294,10 @@ pub trait Backend {
/// # Example
///
/// ```rust
/// # use ratatui::{prelude::*, backend::TestBackend};
/// let backend = TestBackend::new(80, 25);
/// # use ratatui::{backend::{TestBackend}};
/// # let backend = TestBackend::new(80, 25);
/// use ratatui::{backend::Backend, layout::Size};
///
/// assert_eq!(backend.size()?, Size::new(80, 25));
/// # std::io::Result::Ok(())
/// ```
@@ -318,6 +312,64 @@ pub trait Backend {
/// Flush any buffered content to the terminal screen.
fn flush(&mut self) -> io::Result<()>;
/// Scroll a region of the screen upwards, where a region is specified by a (half-open) range
/// of rows.
///
/// Each row in the region is replaced by the row `line_count` rows below it, except the bottom
/// `line_count` rows, which are replaced by empty rows. If `line_count` is equal to or larger
/// than the number of rows in the region, then all rows are replaced with empty rows.
///
/// If the region includes row 0, then `line_count` rows are copied into the bottom of the
/// scrollback buffer. These rows are first taken from the old contents of the region, starting
/// from the top. If there aren't sufficient rows in the region, then the remainder are empty
/// rows.
///
/// The position of the cursor afterwards is undefined.
///
/// The behavior is designed to match what ANSI terminals do when scrolling regions are
/// established. With ANSI terminals, a scrolling region can be established with the "^[[X;Yr"
/// sequence, where X and Y define the lines of the region. The scrolling region can be reset
/// to be the whole screen with the "^[[r" sequence.
///
/// When a scrolling region is established in an ANSI terminal, various operations' behaviors
/// are changed in such a way that the scrolling region acts like a "virtual screen". In
/// particular, the scrolling sequence "^[[NS", which scrolls lines up by a count of N.
///
/// On an ANSI terminal, this method will probably translate to something like:
/// "^[[X;Yr^[[NS^[[r". That is, set the scrolling region, scroll up, then reset the scrolling
/// region.
///
/// For examples of how this function is expected to work, refer to the tests for
/// [`TestBackend::scroll_region_up`].
#[cfg(feature = "scrolling-regions")]
fn scroll_region_up(&mut self, region: std::ops::Range<u16>, line_count: u16)
-> io::Result<()>;
/// Scroll a region of the screen downwards, where a region is specified by a (half-open) range
/// of rows.
///
/// Each row in the region is replaced by the row `line_count` rows above it, except the top
/// `line_count` rows, which are replaced by empty rows. If `line_count` is equal to or larger
/// than the number of rows in the region, then all rows are replaced with empty rows.
///
/// The position of the cursor afterwards is undefined.
///
/// See the documentation for [`Self::scroll_region_down`] for more information about how this
/// is expected to be implemented for ANSI terminals. All of that applies, except the ANSI
/// sequence to scroll down is "^[[NT".
///
/// This function is asymmetrical with regards to the scrollback buffer. The reason is that
/// this how terminals seem to implement things.
///
/// For examples of how this function is expected to work, refer to the tests for
/// [`TestBackend::scroll_region_down`].
#[cfg(feature = "scrolling-regions")]
fn scroll_region_down(
&mut self,
region: std::ops::Range<u16>,
line_count: u16,
) -> io::Result<()>;
}
#[cfg(test)]

View File

@@ -3,7 +3,7 @@
use std::{
fmt::{self, Write},
io,
io, iter,
};
use unicode_width::UnicodeWidthStr;
@@ -24,7 +24,7 @@ use crate::{
/// # Example
///
/// ```rust
/// use ratatui::{backend::TestBackend, prelude::*};
/// use ratatui::backend::{Backend, TestBackend};
///
/// let mut backend = TestBackend::new(10, 2);
/// backend.clear()?;
@@ -35,6 +35,7 @@ use crate::{
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TestBackend {
buffer: Buffer,
scrollback: Buffer,
cursor: bool,
pos: (u16, u16),
}
@@ -73,6 +74,29 @@ impl TestBackend {
pub fn new(width: u16, height: u16) -> Self {
Self {
buffer: Buffer::empty(Rect::new(0, 0, width, height)),
scrollback: Buffer::empty(Rect::new(0, 0, width, 0)),
cursor: false,
pos: (0, 0),
}
}
/// Creates a new `TestBackend` with the specified lines as the initial screen state.
///
/// The backend's screen size is determined from the initial lines.
#[must_use]
pub fn with_lines<'line, Lines>(lines: Lines) -> Self
where
Lines: IntoIterator,
Lines::Item: Into<crate::text::Line<'line>>,
{
let buffer = Buffer::with_lines(lines);
let scrollback = Buffer::empty(Rect {
width: buffer.area.width,
..Rect::ZERO
});
Self {
buffer,
scrollback,
cursor: false,
pos: (0, 0),
}
@@ -83,9 +107,29 @@ impl TestBackend {
&self.buffer
}
/// Returns a reference to the internal scrollback buffer of the `TestBackend`.
///
/// The scrollback buffer represents the part of the screen that is currently hidden from view,
/// but that could be accessed by scrolling back in the terminal's history. This would normally
/// be done using the terminal's scrollbar or an equivalent keyboard shortcut.
///
/// The scrollback buffer starts out empty. Lines are appended when they scroll off the top of
/// the main buffer. This happens when lines are appended to the bottom of the main buffer
/// using [`Backend::append_lines`].
///
/// The scrollback buffer has a maximum height of [`u16::MAX`]. If lines are appended to the
/// bottom of the scrollback buffer when it is at its maximum height, a corresponding number of
/// lines will be removed from the top.
pub const fn scrollback(&self) -> &Buffer {
&self.scrollback
}
/// Resizes the `TestBackend` to the specified width and height.
pub fn resize(&mut self, width: u16, height: u16) {
self.buffer.resize(Rect::new(0, 0, width, height));
let scrollback_height = self.scrollback.area.height;
self.scrollback
.resize(Rect::new(0, 0, width, scrollback_height));
}
/// Asserts that the `TestBackend`'s buffer is equal to the expected buffer.
@@ -93,6 +137,7 @@ impl TestBackend {
/// This is a shortcut for `assert_eq!(self.buffer(), &expected)`.
///
/// # Panics
///
/// When they are not equal, a panic occurs with a detailed error message showing the
/// differences between the expected and actual buffers.
#[allow(deprecated)]
@@ -102,11 +147,42 @@ impl TestBackend {
crate::assert_buffer_eq!(&self.buffer, expected);
}
/// Asserts that the `TestBackend`'s scrollback buffer is equal to the expected buffer.
///
/// This is a shortcut for `assert_eq!(self.scrollback(), &expected)`.
///
/// # Panics
///
/// When they are not equal, a panic occurs with a detailed error message showing the
/// differences between the expected and actual buffers.
#[track_caller]
pub fn assert_scrollback(&self, expected: &Buffer) {
assert_eq!(&self.scrollback, expected);
}
/// Asserts that the `TestBackend`'s scrollback buffer is empty.
///
/// # Panics
///
/// When the scrollback buffer is not equal, a panic occurs with a detailed error message
/// showing the differences between the expected and actual buffers.
pub fn assert_scrollback_empty(&self) {
let expected = Buffer {
area: Rect {
width: self.scrollback.area.width,
..Rect::ZERO
},
content: vec![],
};
self.assert_scrollback(&expected);
}
/// Asserts that the `TestBackend`'s buffer is equal to the expected lines.
///
/// This is a shortcut for `assert_eq!(self.buffer(), &Buffer::with_lines(expected))`.
///
/// # Panics
///
/// When they are not equal, a panic occurs with a detailed error message showing the
/// differences between the expected and actual buffers.
#[track_caller]
@@ -118,11 +194,29 @@ impl TestBackend {
self.assert_buffer(&Buffer::with_lines(expected));
}
/// Asserts that the `TestBackend`'s scrollback buffer is equal to the expected lines.
///
/// This is a shortcut for `assert_eq!(self.scrollback(), &Buffer::with_lines(expected))`.
///
/// # Panics
///
/// When they are not equal, a panic occurs with a detailed error message showing the
/// differences between the expected and actual buffers.
#[track_caller]
pub fn assert_scrollback_lines<'line, Lines>(&self, expected: Lines)
where
Lines: IntoIterator,
Lines::Item: Into<crate::text::Line<'line>>,
{
self.assert_scrollback(&Buffer::with_lines(expected));
}
/// Asserts that the `TestBackend`'s cursor position is equal to the expected one.
///
/// This is a shortcut for `assert_eq!(self.get_cursor_position().unwrap(), expected)`.
///
/// # Panics
///
/// When they are not equal, a panic occurs with a detailed error message showing the
/// differences between the expected and actual position.
#[track_caller]
@@ -175,7 +269,7 @@ impl Backend for TestBackend {
Ok(())
}
fn clear_region(&mut self, clear_type: super::ClearType) -> io::Result<()> {
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
let region = match clear_type {
ClearType::All => return self.clear(),
ClearType::AfterCursor => {
@@ -215,7 +309,7 @@ impl Backend for TestBackend {
/// the cursor y position then that number of empty lines (at most the buffer's height in this
/// case but this limit is instead replaced with scrolling in most backend implementations) will
/// be added after the current position and the cursor will be moved to the last row.
fn append_lines(&mut self, n: u16) -> io::Result<()> {
fn append_lines(&mut self, line_count: u16) -> io::Result<()> {
let Position { x: cur_x, y: cur_y } = self.get_cursor_position()?;
let Rect { width, height, .. } = self.buffer.area;
@@ -224,19 +318,29 @@ impl Backend for TestBackend {
let max_y = height.saturating_sub(1);
let lines_after_cursor = max_y.saturating_sub(cur_y);
if n > lines_after_cursor {
let rotate_by = n.saturating_sub(lines_after_cursor).min(max_y);
if rotate_by == height - 1 {
self.clear()?;
}
if line_count > lines_after_cursor {
// We need to insert blank lines at the bottom and scroll the lines from the top into
// scrollback.
let scroll_by: usize = (line_count - lines_after_cursor).into();
let width: usize = self.buffer.area.width.into();
let cells_to_scrollback = self.buffer.content.len().min(width * scroll_by);
self.set_cursor_position(Position { x: 0, y: rotate_by })?;
self.clear_region(ClearType::BeforeCursor)?;
self.buffer.content.rotate_left((width * rotate_by).into());
append_to_scrollback(
&mut self.scrollback,
self.buffer.content.splice(
0..cells_to_scrollback,
iter::repeat_with(Default::default).take(cells_to_scrollback),
),
);
self.buffer.content.rotate_left(cells_to_scrollback);
append_to_scrollback(
&mut self.scrollback,
iter::repeat_with(Default::default).take(width * scroll_by - cells_to_scrollback),
);
}
let new_cursor_y = cur_y.saturating_add(n).min(max_y);
let new_cursor_y = cur_y.saturating_add(line_count).min(max_y);
self.set_cursor_position(Position::new(new_cursor_x, new_cursor_y))?;
Ok(())
@@ -261,10 +365,98 @@ impl Backend for TestBackend {
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
#[cfg(feature = "scrolling-regions")]
fn scroll_region_up(&mut self, region: std::ops::Range<u16>, scroll_by: u16) -> io::Result<()> {
let width: usize = self.buffer.area.width.into();
let cell_region_start = width * region.start.min(self.buffer.area.height) as usize;
let cell_region_end = width * region.end.min(self.buffer.area.height) as usize;
let cell_region_len = cell_region_end - cell_region_start;
let cells_to_scroll_by = width * scroll_by as usize;
// Deal with the simple case where nothing needs to be copied into scrollback.
if cell_region_start > 0 {
if cells_to_scroll_by >= cell_region_len {
// The scroll amount is large enough to clear the whole region.
self.buffer.content[cell_region_start..cell_region_end].fill_with(Default::default);
} else {
// Scroll up by rotating, then filling in the bottom with empty cells.
self.buffer.content[cell_region_start..cell_region_end]
.rotate_left(cells_to_scroll_by);
self.buffer.content[cell_region_end - cells_to_scroll_by..cell_region_end]
.fill_with(Default::default);
}
return Ok(());
}
// The rows inserted into the scrollback will first come from the buffer, and if that is
// insufficient, will then be blank rows.
let cells_from_region = cell_region_len.min(cells_to_scroll_by);
append_to_scrollback(
&mut self.scrollback,
self.buffer.content.splice(
0..cells_from_region,
iter::repeat_with(Default::default).take(cells_from_region),
),
);
if cells_to_scroll_by < cell_region_len {
// Rotate the remaining cells to the front of the region.
self.buffer.content[cell_region_start..cell_region_end].rotate_left(cells_from_region);
} else {
// Splice cleared out the region. Insert empty rows in scrollback.
append_to_scrollback(
&mut self.scrollback,
iter::repeat_with(Default::default).take(cells_to_scroll_by - cell_region_len),
);
}
Ok(())
}
#[cfg(feature = "scrolling-regions")]
fn scroll_region_down(
&mut self,
region: std::ops::Range<u16>,
scroll_by: u16,
) -> io::Result<()> {
let width: usize = self.buffer.area.width.into();
let cell_region_start = width * region.start.min(self.buffer.area.height) as usize;
let cell_region_end = width * region.end.min(self.buffer.area.height) as usize;
let cell_region_len = cell_region_end - cell_region_start;
let cells_to_scroll_by = width * scroll_by as usize;
if cells_to_scroll_by >= cell_region_len {
// The scroll amount is large enough to clear the whole region.
self.buffer.content[cell_region_start..cell_region_end].fill_with(Default::default);
} else {
// Scroll up by rotating, then filling in the top with empty cells.
self.buffer.content[cell_region_start..cell_region_end]
.rotate_right(cells_to_scroll_by);
self.buffer.content[cell_region_start..cell_region_start + cells_to_scroll_by]
.fill_with(Default::default);
}
Ok(())
}
}
/// Append the provided cells to the bottom of a scrollback buffer. The number of cells must be a
/// multiple of the buffer's width. If the scrollback buffer ends up larger than 65535 lines tall,
/// then lines will be removed from the top to get it down to size.
fn append_to_scrollback(scrollback: &mut Buffer, cells: impl IntoIterator<Item = Cell>) {
scrollback.content.extend(cells);
let width = scrollback.area.width as usize;
let new_height = (scrollback.content.len() / width).min(u16::MAX as usize);
let keep_from = scrollback
.content
.len()
.saturating_sub(width * u16::MAX as usize);
scrollback.content.drain(0..keep_from);
scrollback.area.height = new_height as u16;
}
#[cfg(test)]
mod tests {
use itertools::Itertools as _;
use super::*;
#[test]
@@ -273,6 +465,7 @@ mod tests {
TestBackend::new(10, 2),
TestBackend {
buffer: Buffer::with_lines([" "; 2]),
scrollback: Buffer::empty(Rect::new(0, 0, 10, 0)),
cursor: false,
pos: (0, 0),
}
@@ -286,12 +479,12 @@ mod tests {
#[test]
fn buffer_view_with_overwrites() {
let multi_byte_char = "👨‍👩‍👧‍👦"; // renders 8 wide
let multi_byte_char = "👨‍👩‍👧‍👦"; // renders 2 wide
let buffer = Buffer::with_lines([multi_byte_char]);
assert_eq!(
buffer_view(&buffer),
format!(
r#""{multi_byte_char}" Hidden by multi-width symbols: [(1, " "), (2, " "), (3, " "), (4, " "), (5, " "), (6, " "), (7, " ")]
r#""{multi_byte_char}" Hidden by multi-width symbols: [(1, " ")]
"#,
)
);
@@ -323,6 +516,13 @@ mod tests {
backend.assert_buffer_lines(["aaaaaaaaaa"; 2]);
}
#[test]
#[should_panic = "assertion `left == right` failed"]
fn assert_scrollback_panics() {
let backend = TestBackend::new(10, 2);
backend.assert_scrollback_lines(["aaaaaaaaaa"; 2]);
}
#[test]
fn display() {
let backend = TestBackend::new(10, 2);
@@ -385,8 +585,7 @@ mod tests {
#[test]
fn clear_region_all() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
@@ -406,8 +605,7 @@ mod tests {
#[test]
fn clear_region_after_cursor() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
@@ -430,8 +628,7 @@ mod tests {
#[test]
fn clear_region_before_cursor() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
@@ -454,8 +651,7 @@ mod tests {
#[test]
fn clear_region_current_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
@@ -478,8 +674,7 @@ mod tests {
#[test]
fn clear_region_until_new_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaaaaaaaa",
@@ -502,8 +697,7 @@ mod tests {
#[test]
fn append_lines_not_at_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -536,12 +730,12 @@ mod tests {
"dddddddddd",
"eeeeeeeeee",
]);
backend.assert_scrollback_empty();
}
#[test]
fn append_lines_at_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -557,13 +751,14 @@ mod tests {
backend.append_lines(1).unwrap();
backend.buffer = Buffer::with_lines([
backend.assert_buffer_lines([
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
" ",
]);
backend.assert_scrollback_lines(["aaaaaaaaaa"]);
// It also moves the cursor to the right, as is common of the behaviour of
// terminals in raw-mode
@@ -572,8 +767,7 @@ mod tests {
#[test]
fn append_multiple_lines_not_at_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -597,12 +791,12 @@ mod tests {
"dddddddddd",
"eeeeeeeeee",
]);
backend.assert_scrollback_empty();
}
#[test]
fn append_multiple_lines_past_last_line() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -624,12 +818,12 @@ mod tests {
" ",
" ",
]);
backend.assert_scrollback_lines(["aaaaaaaaaa", "bbbbbbbbbb"]);
}
#[test]
fn append_multiple_lines_where_cursor_at_end_appends_height_lines() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -651,12 +845,18 @@ mod tests {
" ",
" ",
]);
backend.assert_scrollback_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
}
#[test]
fn append_multiple_lines_where_cursor_appends_height_lines() {
let mut backend = TestBackend::new(10, 5);
backend.buffer = Buffer::with_lines([
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
@@ -676,6 +876,114 @@ mod tests {
"eeeeeeeeee",
" ",
]);
backend.assert_scrollback_lines(["aaaaaaaaaa"]);
}
#[test]
fn append_multiple_lines_where_cursor_at_end_appends_more_than_height_lines() {
let mut backend = TestBackend::with_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
]);
backend
.set_cursor_position(Position { x: 0, y: 4 })
.unwrap();
backend.append_lines(8).unwrap();
backend.assert_cursor_position(Position { x: 1, y: 4 });
backend.assert_buffer_lines([
" ",
" ",
" ",
" ",
" ",
]);
backend.assert_scrollback_lines([
"aaaaaaaaaa",
"bbbbbbbbbb",
"cccccccccc",
"dddddddddd",
"eeeeeeeeee",
" ",
" ",
" ",
]);
}
#[test]
fn append_lines_truncates_beyond_u16_max() -> io::Result<()> {
let mut backend = TestBackend::new(10, 5);
// Fill the scrollback with 65535 + 10 lines.
let row_count = u16::MAX as usize + 10;
for row in 0..=row_count {
if row > 4 {
backend.set_cursor_position(Position { x: 0, y: 4 })?;
backend.append_lines(1)?;
}
let cells = format!("{row:>10}").chars().map(Cell::from).collect_vec();
let content = cells
.iter()
.enumerate()
.map(|(column, cell)| (column as u16, 4.min(row) as u16, cell));
backend.draw(content)?;
}
// check that the buffer contains the last 5 lines appended
backend.assert_buffer_lines([
" 65541",
" 65542",
" 65543",
" 65544",
" 65545",
]);
// TODO: ideally this should be something like:
// let lines = (6..=65545).map(|row| format!("{row:>10}"));
// backend.assert_scrollback_lines(lines);
// but there's some truncation happening in Buffer::with_lines that needs to be fixed
assert_eq!(
Buffer {
area: Rect::new(0, 0, 10, 5),
content: backend.scrollback.content[0..10 * 5].to_vec(),
},
Buffer::with_lines([
" 6",
" 7",
" 8",
" 9",
" 10",
]),
"first 5 lines of scrollback should have been truncated"
);
assert_eq!(
Buffer {
area: Rect::new(0, 0, 10, 5),
content: backend.scrollback.content[10 * 65530..10 * 65535].to_vec(),
},
Buffer::with_lines([
" 65536",
" 65537",
" 65538",
" 65539",
" 65540",
]),
"last 5 lines of scrollback should have been appended"
);
// These checks come after the content checks as otherwise we won't see the failing content
// when these checks fail.
// Make sure the scrollback is the right size.
assert_eq!(backend.scrollback.area.width, 10);
assert_eq!(backend.scrollback.area.height, 65535);
assert_eq!(backend.scrollback.content.len(), 10 * 65535);
Ok(())
}
#[test]
@@ -689,4 +997,81 @@ mod tests {
let mut backend = TestBackend::new(10, 2);
backend.flush().unwrap();
}
#[cfg(feature = "scrolling-regions")]
mod scrolling_regions {
use rstest::rstest;
use super::*;
const A: &str = "aaaa";
const B: &str = "bbbb";
const C: &str = "cccc";
const D: &str = "dddd";
const E: &str = "eeee";
const S: &str = " ";
#[rstest]
#[case([A, B, C, D, E], 0..5, 0, [], [A, B, C, D, E])]
#[case([A, B, C, D, E], 0..5, 2, [A, B], [C, D, E, S, S])]
#[case([A, B, C, D, E], 0..5, 5, [A, B, C, D, E], [S, S, S, S, S])]
#[case([A, B, C, D, E], 0..5, 7, [A, B, C, D, E, S, S], [S, S, S, S, S])]
#[case([A, B, C, D, E], 0..3, 0, [], [A, B, C, D, E])]
#[case([A, B, C, D, E], 0..3, 2, [A, B], [C, S, S, D, E])]
#[case([A, B, C, D, E], 0..3, 3, [A, B, C], [S, S, S, D, E])]
#[case([A, B, C, D, E], 0..3, 4, [A, B, C, S], [S, S, S, D, E])]
#[case([A, B, C, D, E], 1..4, 0, [], [A, B, C, D, E])]
#[case([A, B, C, D, E], 1..4, 2, [], [A, D, S, S, E])]
#[case([A, B, C, D, E], 1..4, 3, [], [A, S, S, S, E])]
#[case([A, B, C, D, E], 1..4, 4, [], [A, S, S, S, E])]
#[case([A, B, C, D, E], 0..0, 0, [], [A, B, C, D, E])]
#[case([A, B, C, D, E], 0..0, 2, [S, S], [A, B, C, D, E])]
#[case([A, B, C, D, E], 2..2, 0, [], [A, B, C, D, E])]
#[case([A, B, C, D, E], 2..2, 2, [], [A, B, C, D, E])]
fn scroll_region_up<const L: usize, const M: usize, const N: usize>(
#[case] initial_screen: [&'static str; L],
#[case] range: std::ops::Range<u16>,
#[case] scroll_by: u16,
#[case] expected_scrollback: [&'static str; M],
#[case] expected_buffer: [&'static str; N],
) {
let mut backend = TestBackend::with_lines(initial_screen);
backend.scroll_region_up(range, scroll_by).unwrap();
if expected_scrollback.is_empty() {
backend.assert_scrollback_empty();
} else {
backend.assert_scrollback_lines(expected_scrollback);
}
backend.assert_buffer_lines(expected_buffer);
}
#[rstest]
#[case([A, B, C, D, E], 0..5, 0, [A, B, C, D, E])]
#[case([A, B, C, D, E], 0..5, 2, [S, S, A, B, C])]
#[case([A, B, C, D, E], 0..5, 5, [S, S, S, S, S])]
#[case([A, B, C, D, E], 0..5, 7, [S, S, S, S, S])]
#[case([A, B, C, D, E], 0..3, 0, [A, B, C, D, E])]
#[case([A, B, C, D, E], 0..3, 2, [S, S, A, D, E])]
#[case([A, B, C, D, E], 0..3, 3, [S, S, S, D, E])]
#[case([A, B, C, D, E], 0..3, 4, [S, S, S, D, E])]
#[case([A, B, C, D, E], 1..4, 0, [A, B, C, D, E])]
#[case([A, B, C, D, E], 1..4, 2, [A, S, S, B, E])]
#[case([A, B, C, D, E], 1..4, 3, [A, S, S, S, E])]
#[case([A, B, C, D, E], 1..4, 4, [A, S, S, S, E])]
#[case([A, B, C, D, E], 0..0, 0, [A, B, C, D, E])]
#[case([A, B, C, D, E], 0..0, 2, [A, B, C, D, E])]
#[case([A, B, C, D, E], 2..2, 0, [A, B, C, D, E])]
#[case([A, B, C, D, E], 2..2, 2, [A, B, C, D, E])]
fn scroll_region_down<const M: usize, const N: usize>(
#[case] initial_screen: [&'static str; M],
#[case] range: std::ops::Range<u16>,
#[case] scroll_by: u16,
#[case] expected_buffer: [&'static str; N],
) {
let mut backend = TestBackend::with_lines(initial_screen);
backend.scroll_region_down(range, scroll_by).unwrap();
backend.assert_scrollback_empty();
backend.assert_buffer_lines(expected_buffer);
}
}
}

View File

@@ -41,7 +41,11 @@ macro_rules! assert_buffer_eq {
#[allow(deprecated)]
#[cfg(test)]
mod tests {
use crate::prelude::*;
use crate::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
};
#[test]
fn assert_buffer_eq_does_not_panic_on_equal_buffers() {

View File

@@ -6,7 +6,12 @@ use std::{
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::{buffer::Cell, layout::Position, prelude::*};
use crate::{
buffer::Cell,
layout::{Position, Rect},
style::Style,
text::{Line, Span},
};
/// A buffer that maps to the desired content of the terminal after the draw call
///
@@ -18,7 +23,7 @@ use crate::{buffer::Cell, layout::Position, prelude::*};
/// # Examples:
///
/// ```
/// use ratatui::{
/// use ratatui_core::{
/// buffer::{Buffer, Cell},
/// layout::{Position, Rect},
/// style::{Color, Style},
@@ -163,7 +168,11 @@ impl Buffer {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
/// use ratatui_core::{
/// buffer::{Buffer, Cell},
/// layout::{Position, Rect},
/// };
///
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
///
/// assert_eq!(buffer.cell(Position::new(0, 0)), Some(&Cell::default()));
@@ -190,7 +199,11 @@ impl Buffer {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
/// use ratatui_core::{
/// buffer::{Buffer, Cell},
/// layout::{Position, Rect},
/// style::{Color, Style},
/// };
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
///
/// if let Some(cell) = buffer.cell_mut(Position::new(0, 0)) {
@@ -214,7 +227,8 @@ impl Buffer {
/// # Examples
///
/// ```
/// # use ratatui::prelude::*;
/// use ratatui_core::{buffer::Buffer, layout::Rect};
///
/// let buffer = Buffer::empty(Rect::new(200, 100, 10, 10));
/// // Global coordinates to the top corner of this buffer's area
/// assert_eq!(buffer.index_of(200, 100), 0);
@@ -225,7 +239,8 @@ impl Buffer {
/// Panics when given an coordinate that is outside of this Buffer's area.
///
/// ```should_panic
/// # use ratatui::prelude::*;
/// use ratatui_core::{buffer::Buffer, layout::Rect};
///
/// let buffer = Buffer::empty(Rect::new(200, 100, 10, 10));
/// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
/// // starts at (200, 100).
@@ -246,7 +261,7 @@ impl Buffer {
///
/// Returns `None` if the given coordinates are outside of the Buffer's area.
///
/// Note that this is private because of <https://github.com/ratatui-org/ratatui/issues/1122>
/// Note that this is private because of <https://github.com/ratatui/ratatui/issues/1122>
#[must_use]
const fn index_of_opt(&self, position: Position) -> Option<usize> {
let area = self.area;
@@ -254,9 +269,10 @@ impl Buffer {
return None;
}
// remove offset
let y = position.y - self.area.y;
let x = position.x - self.area.x;
Some((y * self.area.width + x) as usize)
let y = (position.y - self.area.y) as usize;
let x = (position.x - self.area.x) as usize;
let width = self.area.width as usize;
Some(y * width + x)
}
/// Returns the (global) coordinates of a cell given its index
@@ -266,7 +282,8 @@ impl Buffer {
/// # Examples
///
/// ```
/// # use ratatui::prelude::*;
/// use ratatui_core::{buffer::Buffer, layout::Rect};
///
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
/// assert_eq!(buffer.pos_of(0), (200, 100));
@@ -278,22 +295,25 @@ impl Buffer {
/// Panics when given an index that is outside the Buffer's content.
///
/// ```should_panic
/// # use ratatui::prelude::*;
/// use ratatui_core::{buffer::Buffer, layout::Rect};
///
/// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
/// let buffer = Buffer::empty(rect);
/// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
/// buffer.pos_of(100); // Panics
/// ```
#[must_use]
pub fn pos_of(&self, i: usize) -> (u16, u16) {
pub fn pos_of(&self, index: usize) -> (u16, u16) {
debug_assert!(
i < self.content.len(),
"Trying to get the coords of a cell outside the buffer: i={i} len={}",
index < self.content.len(),
"Trying to get the coords of a cell outside the buffer: i={index} len={}",
self.content.len()
);
let x = index % self.area.width as usize + self.area.x as usize;
let y = index / self.area.width as usize + self.area.y as usize;
(
self.area.x + (i as u16) % self.area.width,
self.area.y + (i as u16) / self.area.width,
u16::try_from(x).expect("x overflow. This should never happen as area.width is u16"),
u16::try_from(y).expect("y overflow. This should never happen as area.height is u16"),
)
}
@@ -377,6 +397,8 @@ impl Buffer {
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// [`Color`]: crate::style::Color
pub fn set_style<S: Into<Style>>(&mut self, area: Rect, style: S) {
let style = style.into();
let area = self.area.intersection(area);
@@ -504,7 +526,11 @@ impl<P: Into<Position>> Index<P> for Buffer {
/// # Examples
///
/// ```
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
/// use ratatui_core::{
/// buffer::{Buffer, Cell},
/// layout::{Position, Rect},
/// };
///
/// let buf = Buffer::empty(Rect::new(0, 0, 10, 10));
/// let cell = &buf[(0, 0)];
/// let cell = &buf[Position::new(0, 0)];
@@ -530,7 +556,11 @@ impl<P: Into<Position>> IndexMut<P> for Buffer {
/// # Examples
///
/// ```
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
/// use ratatui_core::{
/// buffer::{Buffer, Cell},
/// layout::{Position, Rect},
/// };
///
/// let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
/// buf[(0, 0)].set_symbol("A");
/// buf[Position::new(0, 0)].set_symbol("B");
@@ -622,6 +652,7 @@ mod tests {
use rstest::{fixture, rstest};
use super::*;
use crate::style::{Color, Modifier, Stylize};
#[test]
fn debug_empty_buffer() {
@@ -1214,11 +1245,12 @@ mod tests {
#[case::shrug("🤷", "🤷xxxxx")]
// Technically this is a (brown) bear, a zero-width joiner and a snowflake
// As it is joined its a single emoji and should therefore have a width of 2.
// It's correctly detected as a single grapheme but it's width is 4 for some reason
#[case::polarbear("🐻‍❄️", "🐻xxx")]
// Prior to unicode-width 0.2, this was incorrectly detected as width 4 for some reason
#[case::polarbear("🐻‍❄️", "🐻xxxxx")]
// Technically this is an eye, a zero-width joiner and a speech bubble
// Both eye and speech bubble include a 'display as emoji' variation selector
#[case::eye_speechbubble("👁️‍🗨️", "👁🗨xxx")]
// Prior to unicode-width 0.2, this was incorrectly detected as width 4 for some reason
#[case::eye_speechbubble("👁️‍🗨️", "👁🗨xxxxx")]
fn renders_emoji(#[case] input: &str, #[case] expected: &str) {
use unicode_width::UnicodeWidthChar;
@@ -1244,4 +1276,24 @@ mod tests {
let expected = Buffer::with_lines([expected]);
assert_eq!(buffer, expected);
}
/// Regression test for <https://github.com/ratatui/ratatui/issues/1441>
///
/// Previously the `pos_of` function would incorrectly cast the index to a u16 value instead of
/// using the index as is. This caused incorrect rendering of any buffer with an length > 65535.
#[test]
fn index_pos_of_u16_max() {
let buffer = Buffer::empty(Rect::new(0, 0, 256, 256 + 1));
assert_eq!(buffer.index_of(255, 255), 65535);
assert_eq!(buffer.pos_of(65535), (255, 255));
assert_eq!(buffer.index_of(0, 256), 65536);
assert_eq!(buffer.pos_of(65536), (0, 256)); // previously (0, 0)
assert_eq!(buffer.index_of(1, 256), 65537);
assert_eq!(buffer.pos_of(65537), (1, 256)); // previously (1, 0)
assert_eq!(buffer.index_of(255, 256), 65791);
assert_eq!(buffer.pos_of(65791), (255, 256)); // previously (255, 0)
}
}

View File

@@ -1,6 +1,6 @@
use compact_str::CompactString;
use crate::prelude::*;
use crate::style::{Color, Modifier, Style};
/// A buffer cell
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
@@ -13,7 +13,7 @@ pub struct Cell {
/// This is a [`CompactString`] which is a wrapper around [`String`] that uses a small inline
/// buffer for short strings.
///
/// See <https://github.com/ratatui-org/ratatui/pull/601> for more information.
/// See <https://github.com/ratatui/ratatui/pull/601> for more information.
symbol: CompactString,
/// The foreground color of the cell.
@@ -157,6 +157,14 @@ impl Default for Cell {
}
}
impl From<char> for Cell {
fn from(ch: char) -> Self {
let mut cell = Self::EMPTY;
cell.set_char(ch);
cell
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -1,4 +1,5 @@
#![warn(clippy::missing_const_for_fn)]
//! Provides types and traits for working with layout and positioning in the terminal.
mod alignment;
mod constraint;
@@ -14,8 +15,8 @@ pub use alignment::Alignment;
pub use constraint::Constraint;
pub use direction::Direction;
pub use flex::Flex;
pub use layout::Layout;
pub use layout::{Layout, Spacing};
pub use margin::Margin;
pub use position::Position;
pub use rect::*;
pub use rect::{Columns, Offset, Positions, Rect, Rows};
pub use size::Size;

View File

@@ -1,6 +1,5 @@
use std::fmt;
use itertools::Itertools;
use strum::EnumIs;
/// A constraint that defines the size of a layout element.
@@ -27,7 +26,8 @@ use strum::EnumIs;
/// `Constraint` provides helper methods to create lists of constraints from various input formats.
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::layout::Constraint;
///
/// // Create a layout with specified lengths for each element
/// let constraints = Constraint::from_lengths([10, 20, 10]);
///
@@ -117,8 +117,12 @@ pub enum Constraint {
/// Applies a percentage of the available space to the element
///
/// Converts the given percentage to a floating-point value and multiplies that with area.
/// This value is rounded back to a integer as part of the layout split calculation.
/// Converts the given percentage to a floating-point value and multiplies that with area. This
/// value is rounded back to a integer as part of the layout split calculation.
///
/// **Note**: As this value only accepts a `u16`, certain percentages that cannot be
/// represented exactly (e.g. 1/3) are not possible. You might want to use
/// [`Constraint::Ratio`] or [`Constraint::Fill`] in such cases.
///
/// # Examples
///
@@ -220,7 +224,8 @@ impl Constraint {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::layout::{Constraint, Layout, Rect};
///
/// # let area = Rect::default();
/// let constraints = Constraint::from_lengths([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
@@ -229,7 +234,7 @@ impl Constraint {
where
T: IntoIterator<Item = u16>,
{
lengths.into_iter().map(Self::Length).collect_vec()
lengths.into_iter().map(Self::Length).collect()
}
/// Convert an iterator of ratios into a vector of constraints
@@ -237,7 +242,8 @@ impl Constraint {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::layout::{Constraint, Layout, Rect};
///
/// # let area = Rect::default();
/// let constraints = Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]);
/// let layout = Layout::default().constraints(constraints).split(area);
@@ -246,10 +252,7 @@ impl Constraint {
where
T: IntoIterator<Item = (u32, u32)>,
{
ratios
.into_iter()
.map(|(n, d)| Self::Ratio(n, d))
.collect_vec()
ratios.into_iter().map(|(n, d)| Self::Ratio(n, d)).collect()
}
/// Convert an iterator of percentages into a vector of constraints
@@ -257,7 +260,8 @@ impl Constraint {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::layout::{Constraint, Layout, Rect};
///
/// # let area = Rect::default();
/// let constraints = Constraint::from_percentages([25, 50, 25]);
/// let layout = Layout::default().constraints(constraints).split(area);
@@ -266,7 +270,7 @@ impl Constraint {
where
T: IntoIterator<Item = u16>,
{
percentages.into_iter().map(Self::Percentage).collect_vec()
percentages.into_iter().map(Self::Percentage).collect()
}
/// Convert an iterator of maxes into a vector of constraints
@@ -274,7 +278,8 @@ impl Constraint {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::layout::{Constraint, Layout, Rect};
///
/// # let area = Rect::default();
/// let constraints = Constraint::from_maxes([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
@@ -283,7 +288,7 @@ impl Constraint {
where
T: IntoIterator<Item = u16>,
{
maxes.into_iter().map(Self::Max).collect_vec()
maxes.into_iter().map(Self::Max).collect()
}
/// Convert an iterator of mins into a vector of constraints
@@ -291,7 +296,8 @@ impl Constraint {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::layout::{Constraint, Layout, Rect};
///
/// # let area = Rect::default();
/// let constraints = Constraint::from_mins([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
@@ -300,7 +306,7 @@ impl Constraint {
where
T: IntoIterator<Item = u16>,
{
mins.into_iter().map(Self::Min).collect_vec()
mins.into_iter().map(Self::Min).collect()
}
/// Convert an iterator of proportional factors into a vector of constraints
@@ -308,7 +314,8 @@ impl Constraint {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::layout::{Constraint, Layout, Rect};
///
/// # let area = Rect::default();
/// let constraints = Constraint::from_fills([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
@@ -317,10 +324,7 @@ impl Constraint {
where
T: IntoIterator<Item = u16>,
{
proportional_factors
.into_iter()
.map(Self::Fill)
.collect_vec()
proportional_factors.into_iter().map(Self::Fill).collect()
}
}
@@ -333,7 +337,8 @@ impl From<u16> for Constraint {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::layout::{Constraint, Direction, Layout, Rect};
///
/// # let area = Rect::default();
/// let layout = Layout::new(Direction::Vertical, [1, 2, 3]).split(area);
/// let layout = Layout::horizontal([1, 2, 3]).split(area);

View File

@@ -1,7 +1,7 @@
use strum::{Display, EnumIs, EnumString};
#[allow(unused_imports)]
use super::constraint::Constraint;
use crate::layout::Constraint;
/// Defines the options for layout flex justify content in a container.
///

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@ use crate::layout::Rect;
/// # Examples
///
/// ```
/// use ratatui::layout::{Position, Rect};
/// use ratatui_core::layout::{Position, Rect};
///
/// // the following are all equivalent
/// let position = Position { x: 1, y: 2 };

View File

@@ -4,8 +4,7 @@ use std::{
fmt,
};
use super::{Position, Size};
use crate::prelude::*;
use crate::layout::{Margin, Position, Size};
mod iter;
pub use iter::*;
@@ -27,7 +26,7 @@ pub struct Rect {
pub height: u16,
}
/// Amounts by which to move a [`Rect`](super::Rect).
/// Amounts by which to move a [`Rect`](crate::layout::Rect).
///
/// Positive numbers move to the right/bottom and negative to the left/top.
///
@@ -56,32 +55,41 @@ impl Rect {
height: 0,
};
/// Creates a new `Rect`, with width and height limited to keep the area under max `u16`. If
/// clipped, aspect ratio will be preserved.
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
let max_area = u16::MAX;
let (clipped_width, clipped_height) =
if u32::from(width) * u32::from(height) > u32::from(max_area) {
let aspect_ratio = f64::from(width) / f64::from(height);
let max_area_f = f64::from(max_area);
let height_f = (max_area_f / aspect_ratio).sqrt();
let width_f = height_f * aspect_ratio;
(width_f as u16, height_f as u16)
} else {
(width, height)
};
/// Creates a new `Rect`, with width and height limited to keep both bounds within `u16`.
///
/// If the width or height would cause the right or bottom coordinate to be larger than the
/// maximum value of `u16`, the width or height will be clamped to keep the right or bottom
/// coordinate within `u16`.
///
/// # Examples
///
/// ```
/// use ratatui_core::layout::Rect;
///
/// let rect = Rect::new(1, 2, 3, 4);
/// ```
pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
// these calculations avoid using min so that this function can be const
let max_width = u16::MAX - x;
let max_height = u16::MAX - y;
let width = if width > max_width { max_width } else { width };
let height = if height > max_height {
max_height
} else {
height
};
Self {
x,
y,
width: clipped_width,
height: clipped_height,
width,
height,
}
}
/// The area of the `Rect`. If the area is larger than the maximum value of `u16`, it will be
/// clamped to `u16::MAX`.
pub const fn area(self) -> u16 {
self.width.saturating_mul(self.height)
pub const fn area(self) -> u32 {
(self.width as u32) * (self.height as u32)
}
/// Returns true if the `Rect` has no area.
@@ -205,7 +213,8 @@ impl Rect {
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, layout::Position};
/// use ratatui_core::layout::{Position, Rect};
///
/// let rect = Rect::new(1, 2, 3, 4);
/// assert!(rect.contains(Position { x: 1, y: 2 }));
/// ````
@@ -234,11 +243,11 @@ impl Rect {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # fn render(frame: &mut Frame) {
/// let area = frame.size();
/// let rect = Rect::new(0, 0, 100, 100).clamp(area);
/// # }
/// use ratatui_core::layout::Rect;
///
/// let area = Rect::new(0, 0, 100, 100);
/// let rect = Rect::new(80, 80, 30, 30).clamp(area);
/// assert_eq!(rect, Rect::new(70, 70, 30, 30));
/// ```
#[must_use = "method returns the modified value"]
pub fn clamp(self, other: Self) -> Self {
@@ -254,7 +263,8 @@ impl Rect {
/// # Example
///
/// ```
/// # use ratatui::prelude::*;
/// use ratatui_core::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
///
/// fn render(area: Rect, buf: &mut Buffer) {
/// for row in area.rows() {
/// Line::raw("Hello, world!").render(row, buf);
@@ -270,10 +280,11 @@ impl Rect {
/// # Example
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui_core::{buffer::Buffer, layout::Rect, text::Text, widgets::Widget};
///
/// fn render(area: Rect, buf: &mut Buffer) {
/// if let Some(left) = area.columns().next() {
/// Block::new().borders(Borders::LEFT).render(left, buf);
/// for (i, column) in area.columns().enumerate() {
/// Text::from(format!("{}", i)).render(column, buf);
/// }
/// }
/// ```
@@ -288,7 +299,8 @@ impl Rect {
/// # Example
///
/// ```
/// # use ratatui::prelude::*;
/// use ratatui_core::{buffer::Buffer, layout::Rect};
///
/// fn render(area: Rect, buf: &mut Buffer) {
/// for position in area.positions() {
/// buf[(position.x, position.y)].set_symbol("x");
@@ -304,7 +316,8 @@ impl Rect {
/// # Examples
///
/// ```
/// # use ratatui::prelude::*;
/// use ratatui_core::layout::Rect;
///
/// let rect = Rect::new(1, 2, 3, 4);
/// let position = rect.as_position();
/// ````
@@ -352,6 +365,7 @@ mod tests {
use rstest::rstest;
use super::*;
use crate::layout::{Constraint, Layout};
#[test]
fn to_string() {
@@ -496,46 +510,28 @@ mod tests {
#[test]
fn size_truncation() {
for width in 256u16..300u16 {
for height in 256u16..300u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert!(rect.width < width || rect.height < height);
// The target dimensions are rounded down so the math will not be too precise
// but let's make sure the ratios don't diverge crazily.
assert!(
(f64::from(rect.width) / f64::from(rect.height)
- f64::from(width) / f64::from(height))
.abs()
< 1.0
);
assert_eq!(
Rect::new(u16::MAX - 100, u16::MAX - 1000, 200, 2000),
Rect {
x: u16::MAX - 100,
y: u16::MAX - 1000,
width: 100,
height: 1000
}
}
// One dimension below 255, one above. Area above max u16.
let width = 900;
let height = 100;
let rect = Rect::new(0, 0, width, height);
assert_ne!(rect.width, 900);
assert_ne!(rect.height, 100);
assert!(rect.width < width || rect.height < height);
);
}
#[test]
fn size_preservation() {
for width in 0..256u16 {
for height in 0..256u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert_eq!(rect.width, width);
assert_eq!(rect.height, height);
assert_eq!(
Rect::new(u16::MAX - 100, u16::MAX - 1000, 100, 1000),
Rect {
x: u16::MAX - 100,
y: u16::MAX - 1000,
width: 100,
height: 1000
}
}
// One dimension below 255, one above. Area below max u16.
let rect = Rect::new(0, 0, 300, 100);
assert_eq!(rect.width, 300);
assert_eq!(rect.height, 100);
);
}
#[test]
@@ -546,7 +542,7 @@ mod tests {
width: 10,
height: 10,
};
const _AREA: u16 = RECT.area();
const _AREA: u32 = RECT.area();
const _LEFT: u16 = RECT.left();
const _RIGHT: u16 = RECT.right();
const _TOP: u16 = RECT.top();

View File

@@ -0,0 +1,329 @@
use crate::layout::{Position, Rect};
/// An iterator over rows within a `Rect`.
pub struct Rows {
/// The `Rect` associated with the rows.
rect: Rect,
/// The y coordinate of the row within the `Rect` when iterating forwards.
current_row_fwd: u16,
/// The y coordinate of the row within the `Rect` when iterating backwards.
current_row_back: u16,
}
impl Rows {
/// Creates a new `Rows` iterator.
pub const fn new(rect: Rect) -> Self {
Self {
rect,
current_row_fwd: rect.y,
current_row_back: rect.bottom(),
}
}
}
impl Iterator for Rows {
type Item = Rect;
/// Retrieves the next row within the `Rect`.
///
/// Returns `None` when there are no more rows to iterate through.
fn next(&mut self) -> Option<Self::Item> {
if self.current_row_fwd >= self.current_row_back {
return None;
}
let row = Rect::new(self.rect.x, self.current_row_fwd, self.rect.width, 1);
self.current_row_fwd += 1;
Some(row)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let start_count = self.current_row_fwd.saturating_sub(self.rect.top());
let end_count = self.rect.bottom().saturating_sub(self.current_row_back);
let count = self
.rect
.height
.saturating_sub(start_count)
.saturating_sub(end_count) as usize;
(count, Some(count))
}
}
impl DoubleEndedIterator for Rows {
/// Retrieves the previous row within the `Rect`.
///
/// Returns `None` when there are no more rows to iterate through.
fn next_back(&mut self) -> Option<Self::Item> {
if self.current_row_back <= self.current_row_fwd {
return None;
}
self.current_row_back -= 1;
let row = Rect::new(self.rect.x, self.current_row_back, self.rect.width, 1);
Some(row)
}
}
/// An iterator over columns within a `Rect`.
pub struct Columns {
/// The `Rect` associated with the columns.
rect: Rect,
/// The x coordinate of the column within the `Rect` when iterating forwards.
current_column_fwd: u16,
/// The x coordinate of the column within the `Rect` when iterating backwards.
current_column_back: u16,
}
impl Columns {
/// Creates a new `Columns` iterator.
pub const fn new(rect: Rect) -> Self {
Self {
rect,
current_column_fwd: rect.x,
current_column_back: rect.right(),
}
}
}
impl Iterator for Columns {
type Item = Rect;
/// Retrieves the next column within the `Rect`.
///
/// Returns `None` when there are no more columns to iterate through.
fn next(&mut self) -> Option<Self::Item> {
if self.current_column_fwd >= self.current_column_back {
return None;
}
let column = Rect::new(self.current_column_fwd, self.rect.y, 1, self.rect.height);
self.current_column_fwd += 1;
Some(column)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let start_count = self.current_column_fwd.saturating_sub(self.rect.left());
let end_count = self.rect.right().saturating_sub(self.current_column_back);
let count = self
.rect
.width
.saturating_sub(start_count)
.saturating_sub(end_count) as usize;
(count, Some(count))
}
}
impl DoubleEndedIterator for Columns {
/// Retrieves the previous column within the `Rect`.
///
/// Returns `None` when there are no more columns to iterate through.
fn next_back(&mut self) -> Option<Self::Item> {
if self.current_column_back <= self.current_column_fwd {
return None;
}
self.current_column_back -= 1;
let column = Rect::new(self.current_column_back, self.rect.y, 1, self.rect.height);
Some(column)
}
}
/// An iterator over positions within a `Rect`.
///
/// The iterator will yield all positions within the `Rect` in a row-major order.
pub struct Positions {
/// The `Rect` associated with the positions.
rect: Rect,
/// The current position within the `Rect`.
current_position: Position,
}
impl Positions {
/// Creates a new `Positions` iterator.
pub const fn new(rect: Rect) -> Self {
Self {
rect,
current_position: Position::new(rect.x, rect.y),
}
}
}
impl Iterator for Positions {
type Item = Position;
/// Retrieves the next position within the `Rect`.
///
/// Returns `None` when there are no more positions to iterate through.
fn next(&mut self) -> Option<Self::Item> {
if self.current_position.y >= self.rect.bottom() {
return None;
}
let position = self.current_position;
self.current_position.x += 1;
if self.current_position.x >= self.rect.right() {
self.current_position.x = self.rect.x;
self.current_position.y += 1;
}
Some(position)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let row_count = self.rect.bottom().saturating_sub(self.current_position.y);
if row_count == 0 {
return (0, Some(0));
}
let column_count = self.rect.right().saturating_sub(self.current_position.x);
// subtract 1 from the row count to account for the current row
let count = (row_count - 1)
.saturating_mul(self.rect.width)
.saturating_add(column_count) as usize;
(count, Some(count))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rows() {
let rect = Rect::new(0, 0, 2, 3);
let mut rows = Rows::new(rect);
assert_eq!(rows.size_hint(), (3, Some(3)));
assert_eq!(rows.next(), Some(Rect::new(0, 0, 2, 1)));
assert_eq!(rows.size_hint(), (2, Some(2)));
assert_eq!(rows.next(), Some(Rect::new(0, 1, 2, 1)));
assert_eq!(rows.size_hint(), (1, Some(1)));
assert_eq!(rows.next(), Some(Rect::new(0, 2, 2, 1)));
assert_eq!(rows.size_hint(), (0, Some(0)));
assert_eq!(rows.next(), None);
assert_eq!(rows.size_hint(), (0, Some(0)));
assert_eq!(rows.next_back(), None);
assert_eq!(rows.size_hint(), (0, Some(0)));
}
#[test]
fn rows_back() {
let rect = Rect::new(0, 0, 2, 3);
let mut rows = Rows::new(rect);
assert_eq!(rows.size_hint(), (3, Some(3)));
assert_eq!(rows.next_back(), Some(Rect::new(0, 2, 2, 1)));
assert_eq!(rows.size_hint(), (2, Some(2)));
assert_eq!(rows.next_back(), Some(Rect::new(0, 1, 2, 1)));
assert_eq!(rows.size_hint(), (1, Some(1)));
assert_eq!(rows.next_back(), Some(Rect::new(0, 0, 2, 1)));
assert_eq!(rows.size_hint(), (0, Some(0)));
assert_eq!(rows.next_back(), None);
assert_eq!(rows.size_hint(), (0, Some(0)));
assert_eq!(rows.next(), None);
assert_eq!(rows.size_hint(), (0, Some(0)));
}
#[test]
fn rows_meet_in_the_middle() {
let rect = Rect::new(0, 0, 2, 4);
let mut rows = Rows::new(rect);
assert_eq!(rows.size_hint(), (4, Some(4)));
assert_eq!(rows.next(), Some(Rect::new(0, 0, 2, 1)));
assert_eq!(rows.size_hint(), (3, Some(3)));
assert_eq!(rows.next_back(), Some(Rect::new(0, 3, 2, 1)));
assert_eq!(rows.size_hint(), (2, Some(2)));
assert_eq!(rows.next(), Some(Rect::new(0, 1, 2, 1)));
assert_eq!(rows.size_hint(), (1, Some(1)));
assert_eq!(rows.next_back(), Some(Rect::new(0, 2, 2, 1)));
assert_eq!(rows.size_hint(), (0, Some(0)));
assert_eq!(rows.next(), None);
assert_eq!(rows.size_hint(), (0, Some(0)));
assert_eq!(rows.next_back(), None);
assert_eq!(rows.size_hint(), (0, Some(0)));
}
#[test]
fn columns() {
let rect = Rect::new(0, 0, 3, 2);
let mut columns = Columns::new(rect);
assert_eq!(columns.size_hint(), (3, Some(3)));
assert_eq!(columns.next(), Some(Rect::new(0, 0, 1, 2)));
assert_eq!(columns.size_hint(), (2, Some(2)));
assert_eq!(columns.next(), Some(Rect::new(1, 0, 1, 2)));
assert_eq!(columns.size_hint(), (1, Some(1)));
assert_eq!(columns.next(), Some(Rect::new(2, 0, 1, 2)));
assert_eq!(columns.size_hint(), (0, Some(0)));
assert_eq!(columns.next(), None);
assert_eq!(columns.size_hint(), (0, Some(0)));
assert_eq!(columns.next_back(), None);
assert_eq!(columns.size_hint(), (0, Some(0)));
}
#[test]
fn columns_back() {
let rect = Rect::new(0, 0, 3, 2);
let mut columns = Columns::new(rect);
assert_eq!(columns.size_hint(), (3, Some(3)));
assert_eq!(columns.next_back(), Some(Rect::new(2, 0, 1, 2)));
assert_eq!(columns.size_hint(), (2, Some(2)));
assert_eq!(columns.next_back(), Some(Rect::new(1, 0, 1, 2)));
assert_eq!(columns.size_hint(), (1, Some(1)));
assert_eq!(columns.next_back(), Some(Rect::new(0, 0, 1, 2)));
assert_eq!(columns.size_hint(), (0, Some(0)));
assert_eq!(columns.next_back(), None);
assert_eq!(columns.size_hint(), (0, Some(0)));
assert_eq!(columns.next(), None);
assert_eq!(columns.size_hint(), (0, Some(0)));
}
#[test]
fn columns_meet_in_the_middle() {
let rect = Rect::new(0, 0, 4, 2);
let mut columns = Columns::new(rect);
assert_eq!(columns.size_hint(), (4, Some(4)));
assert_eq!(columns.next(), Some(Rect::new(0, 0, 1, 2)));
assert_eq!(columns.size_hint(), (3, Some(3)));
assert_eq!(columns.next_back(), Some(Rect::new(3, 0, 1, 2)));
assert_eq!(columns.size_hint(), (2, Some(2)));
assert_eq!(columns.next(), Some(Rect::new(1, 0, 1, 2)));
assert_eq!(columns.size_hint(), (1, Some(1)));
assert_eq!(columns.next_back(), Some(Rect::new(2, 0, 1, 2)));
assert_eq!(columns.size_hint(), (0, Some(0)));
assert_eq!(columns.next(), None);
assert_eq!(columns.size_hint(), (0, Some(0)));
assert_eq!(columns.next_back(), None);
assert_eq!(columns.size_hint(), (0, Some(0)));
}
/// We allow a total of `65536` columns in the range `(0..=65535)`. In this test we iterate
/// forward and skip the first `65534` columns, and expect the next column to be `65535` and
/// the subsequent columns to be `None`.
#[test]
fn columns_max() {
let rect = Rect::new(0, 0, u16::MAX, 1);
let mut columns = Columns::new(rect).skip(usize::from(u16::MAX - 1));
assert_eq!(columns.next(), Some(Rect::new(u16::MAX - 1, 0, 1, 1)));
assert_eq!(columns.next(), None);
}
/// We allow a total of `65536` columns in the range `(0..=65535)`. In this test we iterate
/// backward and skip the last `65534` columns, and expect the next column to be `0` and the
/// subsequent columns to be `None`.
#[test]
fn columns_min() {
let rect = Rect::new(0, 0, u16::MAX, 1);
let mut columns = Columns::new(rect).rev().skip(usize::from(u16::MAX - 1));
assert_eq!(columns.next(), Some(Rect::new(0, 0, 1, 1)));
assert_eq!(columns.next(), None);
assert_eq!(columns.next(), None);
}
#[test]
fn positions() {
let rect = Rect::new(0, 0, 2, 2);
let mut positions = Positions::new(rect);
assert_eq!(positions.size_hint(), (4, Some(4)));
assert_eq!(positions.next(), Some(Position::new(0, 0)));
assert_eq!(positions.size_hint(), (3, Some(3)));
assert_eq!(positions.next(), Some(Position::new(1, 0)));
assert_eq!(positions.size_hint(), (2, Some(2)));
assert_eq!(positions.next(), Some(Position::new(0, 1)));
assert_eq!(positions.size_hint(), (1, Some(1)));
assert_eq!(positions.next(), Some(Position::new(1, 1)));
assert_eq!(positions.size_hint(), (0, Some(0)));
assert_eq!(positions.next(), None);
assert_eq!(positions.size_hint(), (0, Some(0)));
}
}

View File

@@ -1,7 +1,7 @@
#![warn(missing_docs)]
use std::fmt;
use crate::prelude::*;
use crate::layout::Rect;
/// A simple size struct
///

47
ratatui-core/src/lib.rs Normal file
View File

@@ -0,0 +1,47 @@
// show the feature flags in the generated documentation
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png",
html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico"
)]
//! **ratatui-core** is the core library of the [ratatui] project,
//! providing the essential building blocks for creating rich terminal user interfaces in Rust.
//!
//! [ratatui]: https://github.com/ratatui/ratatui
//!
//! ## Why `ratatui-core`?
//!
//! The `ratatui-core` crate is split from the main [`ratatui`](https://crates.io/crates/ratatui) crate
//! to offer better stability for widget library authors. Widget libraries should generally depend
//! on `ratatui-core`, benefiting from a stable API and reducing the need for frequent updates.
//!
//! Applications, on the other hand, should depend on the main `ratatui` crate, which includes
//! built-in widgets and additional features.
//!
//! # Installation
//!
//! Add `ratatui-core` to your `Cargo.toml`:
//!
//! ```shell
//! cargo add ratatui-core
//! ```
#![cfg_attr(feature = "document-features", doc = "\n## Features")]
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
//!
//! # Contributing
//!
//! We welcome contributions from the community! Please see our [CONTRIBUTING](../CONTRIBUTING.md)
//! guide for more details on how to get involved.
//!
//! ## License
//!
//! This project is licensed under the MIT License. See the [LICENSE](../LICENSE) file for details.
pub mod backend;
pub mod buffer;
pub mod layout;
pub mod style;
pub mod symbols;
pub mod text;
pub mod widgets;

View File

@@ -13,7 +13,10 @@
//! ## Example
//!
//! ```
//! use ratatui::prelude::*;
//! use ratatui_core::{
//! style::{Color, Modifier, Style},
//! text::Span,
//! };
//!
//! let heading_style = Style::new()
//! .fg(Color::Black)
@@ -35,13 +38,15 @@
//! - [`Span`]s can be styled again, which will merge the styles.
//! - Many widget types can be styled directly rather than calling their `style()` method.
//!
//! See the [`Stylize`] and [`Styled`] traits for more information. These traits are re-exported in
//! the [`prelude`] module for convenience.
//! See the [`Stylize`] and [`Styled`] traits for more information.
//!
//! ## Example
//!
//! ```
//! use ratatui::{prelude::*, widgets::*};
//! use ratatui_core::{
//! style::{Color, Modifier, Style, Stylize},
//! text::{Span, Text},
//! };
//!
//! assert_eq!(
//! "hello".red().on_blue().bold(),
@@ -55,8 +60,8 @@
//! );
//!
//! assert_eq!(
//! Paragraph::new("hello").red().on_blue().bold(),
//! Paragraph::new("hello").style(
//! Text::from("hello").red().on_blue().bold(),
//! Text::from("hello").style(
//! Style::default()
//! .fg(Color::Red)
//! .bg(Color::Blue)
@@ -65,13 +70,13 @@
//! );
//! ```
//!
//! [`prelude`]: crate::prelude
//! [`Span`]: crate::text::Span
use std::fmt;
use bitflags::bitflags;
pub use color::{Color, ParseColorError};
use stylize::ColorDebugKind;
pub use stylize::{Styled, Stylize};
mod color;
@@ -91,7 +96,7 @@ bitflags! {
/// ## Examples
///
/// ```rust
/// use ratatui::{prelude::*};
/// use ratatui_core::style::Modifier;
///
/// let m = Modifier::BOLD | Modifier::ITALIC;
/// ```
@@ -127,7 +132,7 @@ impl fmt::Debug for Modifier {
/// Style lets you control the main characteristics of the displayed elements.
///
/// ```rust
/// use ratatui::prelude::*;
/// use ratatui_core::style::{Color, Modifier, Style};
///
/// Style::default()
/// .fg(Color::Black)
@@ -138,7 +143,8 @@ impl fmt::Debug for Modifier {
/// Styles can also be created with a [shorthand notation](crate::style#using-style-shorthands).
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::style::{Style, Stylize};
///
/// Style::new().black().on_green().italic().bold();
/// ```
///
@@ -148,7 +154,11 @@ impl fmt::Debug for Modifier {
/// anywhere that accepts `Into<Style>`.
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::{
/// style::{Color, Modifier, Style},
/// text::Line,
/// };
///
/// Line::styled("hello", Style::new().fg(Color::Red));
/// // simplifies to
/// Line::styled("hello", Color::Red);
@@ -163,7 +173,11 @@ impl fmt::Debug for Modifier {
/// just S3.
///
/// ```rust
/// use ratatui::prelude::*;
/// use ratatui_core::{
/// buffer::Buffer,
/// layout::Rect,
/// style::{Color, Modifier, Style},
/// };
///
/// let styles = [
/// Style::default()
@@ -199,7 +213,11 @@ impl fmt::Debug for Modifier {
/// reset all properties until that point use [`Style::reset`].
///
/// ```
/// use ratatui::prelude::*;
/// use ratatui_core::{
/// buffer::Buffer,
/// layout::Rect,
/// style::{Color, Modifier, Style},
/// };
///
/// let styles = [
/// Style::default()
@@ -223,17 +241,32 @@ impl fmt::Debug for Modifier {
/// buffer[(0, 0)].style(),
/// );
/// ```
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[derive(Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Style {
/// The foreground color.
pub fg: Option<Color>,
/// The background color.
pub bg: Option<Color>,
/// The underline color.
#[cfg(feature = "underline-color")]
pub underline_color: Option<Color>,
/// The modifiers to add.
pub add_modifier: Modifier,
/// The modifiers to remove.
pub sub_modifier: Modifier,
}
/// A custom debug implementation that prints only the fields that are not the default, and unwraps
/// the `Option`s.
impl fmt::Debug for Style {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("Style::new()")?;
self.fmt_stylize(f)?;
Ok(())
}
}
impl Styled for Style {
type Item = Self;
@@ -247,6 +280,7 @@ impl Styled for Style {
}
impl Style {
/// Returns a `Style` with default properties.
pub const fn new() -> Self {
Self {
fg: None,
@@ -275,7 +309,8 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::style::{Color, Style};
///
/// let style = Style::default().fg(Color::Blue);
/// let diff = Style::default().fg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
@@ -291,7 +326,8 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::style::{Color, Style};
///
/// let style = Style::default().bg(Color::Blue);
/// let diff = Style::default().bg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
@@ -315,7 +351,8 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::style::{Color, Modifier, Style};
///
/// let style = Style::default()
/// .underline_color(Color::Blue)
/// .add_modifier(Modifier::UNDERLINED);
@@ -343,7 +380,8 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::style::{Modifier, Style};
///
/// let style = Style::default().add_modifier(Modifier::BOLD);
/// let diff = Style::default().add_modifier(Modifier::ITALIC);
/// let patched = style.patch(diff);
@@ -364,7 +402,8 @@ impl Style {
/// ## Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::style::{Modifier, Style};
///
/// let style = Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC);
/// let diff = Style::default().remove_modifier(Modifier::ITALIC);
/// let patched = style.patch(diff);
@@ -386,7 +425,8 @@ impl Style {
///
/// ## Examples
/// ```
/// # use ratatui::prelude::*;
/// use ratatui_core::style::{Color, Modifier, Style};
///
/// let style_1 = Style::default().fg(Color::Yellow);
/// let style_2 = Style::default().bg(Color::Red);
/// let combined = style_1.patch(style_2);
@@ -413,6 +453,54 @@ impl Style {
self
}
/// Formats the style in a way that can be copy-pasted into code using the style shorthands.
///
/// This is useful for debugging and for generating code snippets.
pub(crate) fn fmt_stylize(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use fmt::Debug;
if let Some(fg) = self.fg {
fg.stylize_debug(ColorDebugKind::Foreground).fmt(f)?;
}
if let Some(bg) = self.bg {
bg.stylize_debug(ColorDebugKind::Background).fmt(f)?;
}
#[cfg(feature = "underline-color")]
if let Some(underline_color) = self.underline_color {
underline_color
.stylize_debug(ColorDebugKind::Underline)
.fmt(f)?;
}
for modifier in self.add_modifier.iter() {
match modifier {
Modifier::BOLD => f.write_str(".bold()")?,
Modifier::DIM => f.write_str(".dim()")?,
Modifier::ITALIC => f.write_str(".italic()")?,
Modifier::UNDERLINED => f.write_str(".underlined()")?,
Modifier::SLOW_BLINK => f.write_str(".slow_blink()")?,
Modifier::RAPID_BLINK => f.write_str(".rapid_blink()")?,
Modifier::REVERSED => f.write_str(".reversed()")?,
Modifier::HIDDEN => f.write_str(".hidden()")?,
Modifier::CROSSED_OUT => f.write_str(".crossed_out()")?,
_ => f.write_fmt(format_args!(".add_modifier(Modifier::{modifier:?})"))?,
}
}
for modifier in self.sub_modifier.iter() {
match modifier {
Modifier::BOLD => f.write_str(".not_bold()")?,
Modifier::DIM => f.write_str(".not_dim()")?,
Modifier::ITALIC => f.write_str(".not_italic()")?,
Modifier::UNDERLINED => f.write_str(".not_underlined()")?,
Modifier::SLOW_BLINK => f.write_str(".not_slow_blink()")?,
Modifier::RAPID_BLINK => f.write_str(".not_rapid_blink()")?,
Modifier::REVERSED => f.write_str(".not_reversed()")?,
Modifier::HIDDEN => f.write_str(".not_hidden()")?,
Modifier::CROSSED_OUT => f.write_str(".not_crossed_out()")?,
_ => f.write_fmt(format_args!(".remove_modifier(Modifier::{modifier:?})"))?,
}
}
Ok(())
}
}
impl From<Color> for Style {
@@ -423,7 +511,8 @@ impl From<Color> for Style {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::style::{Color, Style};
///
/// let style = Style::from(Color::Red);
/// ```
fn from(color: Color) -> Self {
@@ -437,7 +526,8 @@ impl From<(Color, Color)> for Style {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::style::{Color, Style};
///
/// // red foreground, blue background
/// let style = Style::from((Color::Red, Color::Blue));
/// // default foreground, blue background
@@ -459,7 +549,8 @@ impl From<Modifier> for Style {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::style::{Style, Modifier};
///
/// // add bold and italic
/// let style = Style::from(Modifier::BOLD|Modifier::ITALIC);
fn from(modifier: Modifier) -> Self {
@@ -473,7 +564,8 @@ impl From<(Modifier, Modifier)> for Style {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::style::{Modifier, Style};
///
/// // add bold and italic, remove dim
/// let style = Style::from((Modifier::BOLD | Modifier::ITALIC, Modifier::DIM));
/// ```
@@ -492,7 +584,8 @@ impl From<(Color, Modifier)> for Style {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::style::{Color, Modifier, Style};
///
/// // red foreground, add bold and italic
/// let style = Style::from((Color::Red, Modifier::BOLD | Modifier::ITALIC));
/// ```
@@ -509,7 +602,8 @@ impl From<(Color, Color, Modifier)> for Style {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::style::{Color, Modifier, Style};
///
/// // red foreground, blue background, add bold and italic
/// let style = Style::from((Color::Red, Color::Blue, Modifier::BOLD | Modifier::ITALIC));
/// ```
@@ -525,7 +619,8 @@ impl From<(Color, Color, Modifier, Modifier)> for Style {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::style::{Color, Modifier, Style};
///
/// // red foreground, blue background, add bold and italic, remove dim
/// let style = Style::from((
/// Color::Red,
@@ -549,6 +644,20 @@ mod tests {
use super::*;
#[rstest]
#[case(Style::new(), "Style::new()")]
#[case(Style::new().red(), "Style::new().red()")]
#[case(Style::new().on_blue(), "Style::new().on_blue()")]
#[case(Style::new().bold(), "Style::new().bold()")]
#[case(Style::new().not_italic(), "Style::new().not_italic()")]
#[case(
Style::new().red().on_blue().bold().italic().not_dim().not_hidden(),
"Style::new().red().on_blue().bold().italic().not_dim().not_hidden()"
)]
fn debug(#[case] style: Style, #[case] expected: &'static str) {
assert_eq!(format!("{style:?}"), expected);
}
#[test]
fn combined_patch_gives_same_result_as_individual_patch() {
let styles = [

View File

@@ -2,6 +2,8 @@
use std::{fmt, str::FromStr};
use crate::style::stylize::{ColorDebug, ColorDebugKind};
/// ANSI Color
///
/// All colors from the [ANSI color table] are supported (though some names are not exactly the
@@ -42,7 +44,7 @@ use std::{fmt, str::FromStr};
/// ```
/// use std::str::FromStr;
///
/// use ratatui::prelude::*;
/// use ratatui_core::style::Color;
///
/// assert_eq!(Color::from_str("red"), Ok(Color::Red));
/// assert_eq!("red".parse(), Ok(Color::Red));
@@ -110,14 +112,12 @@ pub enum Color {
/// Notably versions of Windows Terminal prior to Windows 10 and macOS Terminal.app do not
/// support this.
///
/// If the terminal does not support true color, code using the [`TermwizBackend`] will
/// If the terminal does not support true color, code using the `TermwizBackend` will
/// fallback to the default text color. Crossterm and Termion do not have this capability and
/// the display will be unpredictable (e.g. Terminal.app may display glitched blinking text).
/// See <https://github.com/ratatui-org/ratatui/issues/475> for an example of this problem.
/// See <https://github.com/ratatui/ratatui/issues/475> for an example of this problem.
///
/// See also: <https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit>
///
/// [`TermwizBackend`]: crate::backend::TermwizBackend
Rgb(u8, u8, u8),
/// An 8-bit 256 color.
///
@@ -166,7 +166,9 @@ impl<'de> serde::Deserialize<'de> for Color {
/// # Examples
///
/// ```
/// use ratatui::prelude::*;
/// use std::str::FromStr;
///
/// use ratatui_core::style::Color;
///
/// #[derive(Debug, serde::Deserialize)]
/// struct Theme {
@@ -261,7 +263,7 @@ impl std::error::Error for ParseColorError {}
/// ```
/// use std::str::FromStr;
///
/// use ratatui::prelude::*;
/// use ratatui_core::style::Color;
///
/// let color: Color = Color::from_str("blue").unwrap();
/// assert_eq!(color, Color::Blue);
@@ -361,111 +363,114 @@ impl fmt::Display for Color {
}
impl Color {
pub(crate) const fn stylize_debug(self, kind: ColorDebugKind) -> ColorDebug {
ColorDebug { kind, color: self }
}
/// Converts a HSL representation to a `Color::Rgb` instance.
///
/// The `from_hsl` function converts the Hue, Saturation and Lightness values to a
/// corresponding `Color` RGB equivalent.
/// The `from_hsl` function converts the Hue, Saturation and Lightness values to a corresponding
/// `Color` RGB equivalent.
///
/// Hue values should be in the range [0, 360].
/// Saturation and L values should be in the range [0, 100].
/// Values that are not in the range are clamped to be within the range.
/// Hue values should be in the range [-180..180]. Values outside this range are normalized by
/// wrapping.
///
/// Saturation and L values should be in the range [0.0..1.0]. Values outside this range are
/// clamped.
///
/// Clamping to valid ranges happens before conversion to RGB.
///
/// # Examples
///
/// ```
/// use ratatui::prelude::*;
/// use palette::Hsl;
/// use ratatui_core::style::Color;
///
/// let color: Color = Color::from_hsl(360.0, 100.0, 100.0);
/// // Minimum Lightness is black
/// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 0.0));
/// assert_eq!(color, Color::Rgb(0, 0, 0));
///
/// // Maximum Lightness is white
/// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 1.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));
/// // Minimum Saturation is fully desaturated red = gray
/// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 0.5));
/// assert_eq!(color, Color::Rgb(128, 128, 128));
///
/// // Bright red
/// let color: Color = Color::from_hsl(Hsl::new(0.0, 1.0, 0.5));
/// assert_eq!(color, Color::Rgb(255, 0, 0));
///
/// // Bright blue
/// let color: Color = Color::from_hsl(Hsl::new(-120.0, 1.0, 0.5));
/// assert_eq!(color, Color::Rgb(0, 0, 255));
/// ```
pub fn from_hsl(h: f64, s: f64, l: f64) -> Self {
// Clamp input values to valid ranges
let h = h.clamp(0.0, 360.0);
let s = s.clamp(0.0, 100.0);
let l = l.clamp(0.0, 100.0);
#[cfg(feature = "palette")]
pub fn from_hsl(hsl: palette::Hsl) -> Self {
use palette::{Clamp, FromColor, Srgb};
let hsl = hsl.clamp();
let Srgb {
red,
green,
blue,
standard: _,
}: Srgb<u8> = Srgb::from_color(hsl).into();
// Delegate to the function for normalized HSL to RGB conversion
normalized_hsl_to_rgb(h / 360.0, s / 100.0, l / 100.0)
}
}
/// Converts normalized HSL (Hue, Saturation, Lightness) values to RGB (Red, Green, Blue) color
/// representation. H, S, and L values should be in the range [0, 1].
///
/// Based on <https://github.com/killercup/hsl-rs/blob/b8a30e11afd75f262e0550725333293805f4ead0/src/lib.rs>
fn normalized_hsl_to_rgb(hue: f64, saturation: f64, lightness: f64) -> Color {
// This function can be made into `const` in the future.
// This comment contains the relevant information for making it `const`.
//
// If it is `const` and made public, users can write the following:
//
// ```rust
// const SLATE_50: Color = normalized_hsl_to_rgb(0.210, 0.40, 0.98);
// ```
//
// For it to be const now, we need `#![feature(const_fn_floating_point_arithmetic)]`
// Tracking issue: https://github.com/rust-lang/rust/issues/57241
//
// We would also need to remove the use of `.round()` in this function, i.e.:
//
// ```rust
// Color::Rgb((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
// ```
// Initialize RGB components
let red: f64;
let green: f64;
let blue: f64;
// Check if the color is achromatic (grayscale)
if saturation == 0.0 {
red = lightness;
green = lightness;
blue = lightness;
} else {
// Calculate RGB components for colored cases
let q = if lightness < 0.5 {
lightness * (1.0 + saturation)
} else {
lightness + saturation - lightness * saturation
};
let p = 2.0 * lightness - q;
red = hue_to_rgb(p, q, hue + 1.0 / 3.0);
green = hue_to_rgb(p, q, hue);
blue = hue_to_rgb(p, q, hue - 1.0 / 3.0);
Self::Rgb(red, green, blue)
}
// Scale RGB components to the range [0, 255] and create a Color::Rgb instance
Color::Rgb(
(red * 255.0).round() as u8,
(green * 255.0).round() as u8,
(blue * 255.0).round() as u8,
)
}
/// Converts a `HSLuv` representation to a `Color::Rgb` instance.
///
/// The `from_hsluv` function converts the Hue, Saturation and Lightness values to a
/// corresponding `Color` RGB equivalent.
///
/// Hue values should be in the range [-180.0..180.0]. Values outside this range are normalized
/// by wrapping.
///
/// Saturation and L values should be in the range [0.0..100.0]. Values outside this range are
/// clamped.
///
/// Clamping to valid ranges happens before conversion to RGB.
///
/// # Examples
///
/// ```
/// use palette::Hsluv;
/// use ratatui_core::style::Color;
///
/// // Minimum Lightness is black
/// let color: Color = Color::from_hsluv(Hsluv::new(0.0, 100.0, 0.0));
/// assert_eq!(color, Color::Rgb(0, 0, 0));
///
/// // Maximum Lightness is white
/// let color: Color = Color::from_hsluv(Hsluv::new(0.0, 0.0, 100.0));
/// assert_eq!(color, Color::Rgb(255, 255, 255));
///
/// // Minimum Saturation is fully desaturated red = gray
/// let color = Color::from_hsluv(Hsluv::new(0.0, 0.0, 50.0));
/// assert_eq!(color, Color::Rgb(119, 119, 119));
///
/// // Bright Red
/// let color = Color::from_hsluv(Hsluv::new(12.18, 100.0, 53.2));
/// assert_eq!(color, Color::Rgb(255, 0, 0));
///
/// // Bright Blue
/// let color = Color::from_hsluv(Hsluv::new(-94.13, 100.0, 32.3));
/// assert_eq!(color, Color::Rgb(0, 0, 255));
/// ```
#[cfg(feature = "palette")]
pub fn from_hsluv(hsluv: palette::Hsluv) -> Self {
use palette::{Clamp, FromColor, Srgb};
let hsluv = hsluv.clamp();
let Srgb {
red,
green,
blue,
standard: _,
}: Srgb<u8> = Srgb::from_color(hsluv).into();
/// Helper function to calculate RGB component for a specific hue value.
fn hue_to_rgb(p: f64, q: f64, t: f64) -> f64 {
// Adjust the hue value to be within the valid range [0, 1]
let mut t = t;
if t < 0.0 {
t += 1.0;
}
if t > 1.0 {
t -= 1.0;
}
// Calculate the RGB component based on the hue value
if t < 1.0 / 6.0 {
p + (q - p) * 6.0 * t
} else if t < 1.0 / 2.0 {
q
} else if t < 2.0 / 3.0 {
p + (q - p) * (2.0 / 3.0 - t) * 6.0
} else {
p
Self::Rgb(red, green, blue)
}
}
@@ -473,36 +478,62 @@ fn hue_to_rgb(p: f64, q: f64, t: f64) -> f64 {
mod tests {
use std::error::Error;
#[cfg(feature = "palette")]
use palette::{Hsl, Hsluv};
#[cfg(feature = "palette")]
use rstest::rstest;
#[cfg(feature = "serde")]
use serde::de::{Deserialize, IntoDeserializer};
use super::*;
#[test]
fn test_hsl_to_rgb() {
// Test with valid HSL values
let color = Color::from_hsl(120.0, 50.0, 75.0);
assert_eq!(color, Color::Rgb(159, 223, 159));
#[cfg(feature = "palette")]
#[rstest]
#[case::black(Hsl::new(0.0, 0.0, 0.0), Color::Rgb(0, 0, 0))]
#[case::white(Hsl::new(0.0, 0.0, 1.0), Color::Rgb(255, 255, 255))]
#[case::valid(Hsl::new(120.0, 0.5, 0.75), Color::Rgb(159, 223, 159))]
#[case::min_hue(Hsl::new(-180.0, 0.5, 0.75), Color::Rgb(159, 223, 223))]
#[case::max_hue(Hsl::new(180.0, 0.5, 0.75), Color::Rgb(159, 223, 223))]
#[case::min_saturation(Hsl::new(0.0, 0.0, 0.5), Color::Rgb(128, 128, 128))]
#[case::max_saturation(Hsl::new(0.0, 1.0, 0.5), Color::Rgb(255, 0, 0))]
#[case::min_lightness(Hsl::new(0.0, 0.5, 0.0), Color::Rgb(0, 0, 0))]
#[case::max_lightness(Hsl::new(0.0, 0.5, 1.0), Color::Rgb(255, 255, 255))]
#[case::under_hue_wraps(Hsl::new(-240.0, 0.5, 0.75), Color::Rgb(159, 223, 159))]
#[case::over_hue_wraps(Hsl::new(480.0, 0.5, 0.75), Color::Rgb(159, 223, 159))]
#[case::under_saturation_clamps(Hsl::new(0.0, -0.5, 0.75), Color::Rgb(191, 191, 191))]
#[case::over_saturation_clamps(Hsl::new(0.0, 1.2, 0.75), Color::Rgb(255, 128, 128))]
#[case::under_lightness_clamps(Hsl::new(0.0, 0.5, -0.20), Color::Rgb(0, 0, 0))]
#[case::over_lightness_clamps(Hsl::new(0.0, 0.5, 1.5), Color::Rgb(255, 255, 255))]
#[case::under_saturation_lightness_clamps(Hsl::new(0.0, -0.5, -0.20), Color::Rgb(0, 0, 0))]
#[case::over_saturation_lightness_clamps(Hsl::new(0.0, 1.2, 1.5), Color::Rgb(255, 255, 255))]
fn test_hsl_to_rgb(#[case] hsl: palette::Hsl, #[case] expected: Color) {
assert_eq!(Color::from_hsl(hsl), expected);
}
// Test with H value at upper bound
let color = Color::from_hsl(360.0, 50.0, 75.0);
assert_eq!(color, Color::Rgb(223, 159, 159));
// Test with H value exceeding the upper bound
let color = Color::from_hsl(400.0, 50.0, 75.0);
assert_eq!(color, Color::Rgb(223, 159, 159));
// Test with S and L values exceeding the upper bound
let color = Color::from_hsl(240.0, 120.0, 150.0);
assert_eq!(color, Color::Rgb(255, 255, 255));
// Test with H, S, and L values below the lower bound
let color = Color::from_hsl(-20.0, -50.0, -20.0);
assert_eq!(color, Color::Rgb(0, 0, 0));
// Test with S and L values below the lower bound
let color = Color::from_hsl(60.0, -20.0, -10.0);
assert_eq!(color, Color::Rgb(0, 0, 0));
#[cfg(feature = "palette")]
#[rstest]
#[case::black(Hsluv::new(0.0, 0.0, 0.0), Color::Rgb(0, 0, 0))]
#[case::white(Hsluv::new(0.0, 0.0, 100.0), Color::Rgb(255, 255, 255))]
#[case::valid(Hsluv::new(120.0, 50.0, 75.0), Color::Rgb(147, 198, 129))]
#[case::min_hue(Hsluv::new(-180.0, 50.0, 75.0), Color::Rgb(135,196, 188))]
#[case::max_hue(Hsluv::new(180.0, 50.0, 75.0), Color::Rgb(135, 196, 188))]
#[case::min_saturation(Hsluv::new(0.0, 0.0, 75.0), Color::Rgb(185, 185, 185))]
#[case::max_saturation(Hsluv::new(0.0, 100.0, 75.0), Color::Rgb(255, 156, 177))]
#[case::min_lightness(Hsluv::new(0.0, 50.0, 0.0), Color::Rgb(0, 0, 0))]
#[case::max_lightness(Hsluv::new(0.0, 50.0, 100.0), Color::Rgb(255, 255, 255))]
#[case::under_hue_wraps(Hsluv::new(-240.0, 50.0, 75.0), Color::Rgb(147, 198, 129))]
#[case::over_hue_wraps(Hsluv::new(480.0, 50.0, 75.0), Color::Rgb(147, 198, 129))]
#[case::under_saturation_clamps(Hsluv::new(0.0, -50.0, 75.0), Color::Rgb(185, 185, 185))]
#[case::over_saturation_clamps(Hsluv::new(0.0, 150.0, 75.0), Color::Rgb(255, 156, 177))]
#[case::under_lightness_clamps(Hsluv::new(0.0, 50.0, -20.0), Color::Rgb(0, 0, 0))]
#[case::over_lightness_clamps(Hsluv::new(0.0, 50.0, 150.0), Color::Rgb(255, 255, 255))]
#[case::under_saturation_lightness_clamps(Hsluv::new(0.0, -50.0, -20.0), Color::Rgb(0, 0, 0))]
#[case::over_saturation_lightness_clamps(
Hsluv::new(0.0, 150.0, 150.0),
Color::Rgb(255, 255, 255)
)]
fn test_hsluv_to_rgb(#[case] hsluv: palette::Hsluv, #[case] expected: Color) {
assert_eq!(Color::from_hsluv(hsluv), expected);
}
#[test]

View File

@@ -403,8 +403,10 @@
//! # Example
//!
//! ```rust
//! # use ratatui::prelude::*;
//! use ratatui::style::palette::material::{BLUE, RED};
//! use ratatui_core::style::{
//! palette::material::{BLUE, RED},
//! Color,
//! };
//!
//! assert_eq!(RED.c500, Color::Rgb(244, 67, 54));
//! assert_eq!(BLUE.c500, Color::Rgb(33, 150, 243));
@@ -412,7 +414,7 @@
//!
//! [`matdesign-color` crate]: https://crates.io/crates/matdesign-color
use crate::prelude::*;
use crate::style::Color;
/// A palette of colors for use in Material design with accent colors
///

View File

@@ -268,14 +268,16 @@
//! # Example
//!
//! ```rust
//! # use ratatui::prelude::*;
//! use ratatui::style::palette::tailwind::{BLUE, RED};
//! use ratatui_core::style::{
//! palette::tailwind::{BLUE, RED},
//! Color,
//! };
//!
//! assert_eq!(RED.c500, Color::Rgb(239, 68, 68));
//! assert_eq!(BLUE.c500, Color::Rgb(59, 130, 246));
//! ```
use crate::prelude::*;
use crate::style::Color;
pub struct Palette {
pub c50: Color,

View File

@@ -7,7 +7,7 @@ use ::palette::{
};
use palette::{stimulus::IntoStimulus, Srgb};
use super::Color;
use crate::style::Color;
/// Convert an [`palette::Srgb`] color to a [`Color`].
///
@@ -15,7 +15,7 @@ use super::Color;
///
/// ```
/// use palette::Srgb;
/// use ratatui::style::Color;
/// use ratatui_core::style::Color;
///
/// let color = Color::from(Srgb::new(1.0f32, 0.0, 0.0));
/// assert_eq!(color, Color::Rgb(255, 0, 0));
@@ -36,7 +36,7 @@ impl<T: IntoStimulus<u8>> From<Srgb<T>> for Color {
///
/// ```
/// use palette::LinSrgb;
/// use ratatui::style::Color;
/// use ratatui_core::style::Color;
///
/// let color = Color::from(LinSrgb::new(1.0f32, 0.0, 0.0));
/// assert_eq!(color, Color::Rgb(255, 0, 0));

View File

@@ -1,3 +1,5 @@
use std::fmt;
use paste::paste;
use crate::{
@@ -23,6 +25,75 @@ pub trait Styled {
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item;
}
/// A helper struct to make it easy to debug using the `Stylize` method names
pub(crate) struct ColorDebug {
pub kind: ColorDebugKind,
pub color: Color,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub(crate) enum ColorDebugKind {
Foreground,
Background,
#[cfg(feature = "underline-color")]
Underline,
}
impl fmt::Debug for ColorDebug {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
#[cfg(feature = "underline-color")]
let is_underline = self.kind == ColorDebugKind::Underline;
#[cfg(not(feature = "underline-color"))]
let is_underline = false;
if is_underline
|| matches!(
self.color,
Color::Reset | Color::Indexed(_) | Color::Rgb(_, _, _)
)
{
match self.kind {
ColorDebugKind::Foreground => write!(f, ".fg(")?,
ColorDebugKind::Background => write!(f, ".bg(")?,
#[cfg(feature = "underline-color")]
ColorDebugKind::Underline => write!(f, ".underline_color(")?,
}
write!(f, "Color::{:?}", self.color)?;
write!(f, ")")?;
return Ok(());
}
match self.kind {
ColorDebugKind::Foreground => write!(f, ".")?,
ColorDebugKind::Background => write!(f, ".on_")?,
// TODO: .underline_color_xxx is not implemented on Stylize yet, but it should be
#[cfg(feature = "underline-color")]
ColorDebugKind::Underline => {
unreachable!("covered by the first part of the if statement")
}
}
match self.color {
Color::Black => write!(f, "black")?,
Color::Red => write!(f, "red")?,
Color::Green => write!(f, "green")?,
Color::Yellow => write!(f, "yellow")?,
Color::Blue => write!(f, "blue")?,
Color::Magenta => write!(f, "magenta")?,
Color::Cyan => write!(f, "cyan")?,
Color::Gray => write!(f, "gray")?,
Color::DarkGray => write!(f, "dark_gray")?,
Color::LightRed => write!(f, "light_red")?,
Color::LightGreen => write!(f, "light_green")?,
Color::LightYellow => write!(f, "light_yellow")?,
Color::LightBlue => write!(f, "light_blue")?,
Color::LightMagenta => write!(f, "light_magenta")?,
Color::LightCyan => write!(f, "light_cyan")?,
Color::White => write!(f, "white")?,
_ => unreachable!("covered by the first part of the if statement"),
}
write!(f, "()")
}
}
/// Generates two methods for each color, one for setting the foreground color (`red()`, `blue()`,
/// etc) and one for setting the background color (`on_red()`, `on_blue()`, etc.). Each method sets
/// the color of the style to the corresponding color.
@@ -124,8 +195,12 @@ macro_rules! modifier {
/// by `not_`). The `reset()` method is also provided to reset the style.
///
/// # Examples
/// ```
/// use ratatui::{prelude::*, widgets::*};
/// ```ignore
/// use ratatui_core::{
/// style::{Color, Modifier, Style, Stylize},
/// text::Line,
/// widgets::{Block, Paragraph},
/// };
///
/// let span = "hello".red().on_blue().bold();
/// let line = Line::from(vec![
@@ -231,6 +306,7 @@ impl Styled for String {
#[cfg(test)]
mod tests {
use itertools::Itertools;
use rstest::rstest;
use super::*;
@@ -346,7 +422,7 @@ mod tests {
// issue as above without the `Styled` trait impl for `String`
let items = [String::from("a"), String::from("b")];
let sss = items.iter().map(|s| format!("{s}{s}").red()).collect_vec();
assert_eq!(sss, vec![Span::from("aa").red(), Span::from("bb").red()]);
assert_eq!(sss, [Span::from("aa").red(), Span::from("bb").red()]);
}
#[test]
@@ -423,4 +499,77 @@ mod tests {
Span::styled("hello", all_modifier_black)
);
}
#[rstest]
#[case(Color::Black, ".black()")]
#[case(Color::Red, ".red()")]
#[case(Color::Green, ".green()")]
#[case(Color::Yellow, ".yellow()")]
#[case(Color::Blue, ".blue()")]
#[case(Color::Magenta, ".magenta()")]
#[case(Color::Cyan, ".cyan()")]
#[case(Color::Gray, ".gray()")]
#[case(Color::DarkGray, ".dark_gray()")]
#[case(Color::LightRed, ".light_red()")]
#[case(Color::LightGreen, ".light_green()")]
#[case(Color::LightYellow, ".light_yellow()")]
#[case(Color::LightBlue, ".light_blue()")]
#[case(Color::LightMagenta, ".light_magenta()")]
#[case(Color::LightCyan, ".light_cyan()")]
#[case(Color::White, ".white()")]
#[case(Color::Indexed(10), ".fg(Color::Indexed(10))")]
#[case(Color::Rgb(255, 0, 0), ".fg(Color::Rgb(255, 0, 0))")]
fn stylize_debug_foreground(#[case] color: Color, #[case] expected: &str) {
let debug = color.stylize_debug(ColorDebugKind::Foreground);
assert_eq!(format!("{debug:?}"), expected);
}
#[rstest]
#[case(Color::Black, ".on_black()")]
#[case(Color::Red, ".on_red()")]
#[case(Color::Green, ".on_green()")]
#[case(Color::Yellow, ".on_yellow()")]
#[case(Color::Blue, ".on_blue()")]
#[case(Color::Magenta, ".on_magenta()")]
#[case(Color::Cyan, ".on_cyan()")]
#[case(Color::Gray, ".on_gray()")]
#[case(Color::DarkGray, ".on_dark_gray()")]
#[case(Color::LightRed, ".on_light_red()")]
#[case(Color::LightGreen, ".on_light_green()")]
#[case(Color::LightYellow, ".on_light_yellow()")]
#[case(Color::LightBlue, ".on_light_blue()")]
#[case(Color::LightMagenta, ".on_light_magenta()")]
#[case(Color::LightCyan, ".on_light_cyan()")]
#[case(Color::White, ".on_white()")]
#[case(Color::Indexed(10), ".bg(Color::Indexed(10))")]
#[case(Color::Rgb(255, 0, 0), ".bg(Color::Rgb(255, 0, 0))")]
fn stylize_debug_background(#[case] color: Color, #[case] expected: &str) {
let debug = color.stylize_debug(ColorDebugKind::Background);
assert_eq!(format!("{debug:?}"), expected);
}
#[cfg(feature = "underline-color")]
#[rstest]
#[case(Color::Black, ".underline_color(Color::Black)")]
#[case(Color::Red, ".underline_color(Color::Red)")]
#[case(Color::Green, ".underline_color(Color::Green)")]
#[case(Color::Yellow, ".underline_color(Color::Yellow)")]
#[case(Color::Blue, ".underline_color(Color::Blue)")]
#[case(Color::Magenta, ".underline_color(Color::Magenta)")]
#[case(Color::Cyan, ".underline_color(Color::Cyan)")]
#[case(Color::Gray, ".underline_color(Color::Gray)")]
#[case(Color::DarkGray, ".underline_color(Color::DarkGray)")]
#[case(Color::LightRed, ".underline_color(Color::LightRed)")]
#[case(Color::LightGreen, ".underline_color(Color::LightGreen)")]
#[case(Color::LightYellow, ".underline_color(Color::LightYellow)")]
#[case(Color::LightBlue, ".underline_color(Color::LightBlue)")]
#[case(Color::LightMagenta, ".underline_color(Color::LightMagenta)")]
#[case(Color::LightCyan, ".underline_color(Color::LightCyan)")]
#[case(Color::White, ".underline_color(Color::White)")]
#[case(Color::Indexed(10), ".underline_color(Color::Indexed(10))")]
#[case(Color::Rgb(255, 0, 0), ".underline_color(Color::Rgb(255, 0, 0))")]
fn stylize_debug_underline(#[case] color: Color, #[case] expected: &str) {
let debug = color.stylize_debug(ColorDebugKind::Underline);
assert_eq!(format!("{debug:?}"), expected);
}
}

View File

@@ -1,3 +1,5 @@
//! Symbols and markers for drawing various widgets.
use strum::{Display, EnumString};
pub mod border;
@@ -155,7 +157,7 @@ pub enum Marker {
}
pub mod scrollbar {
use super::{block, line};
use crate::symbols::{block, line};
/// Scrollbar Set
/// ```text
@@ -203,6 +205,14 @@ pub mod scrollbar {
};
}
pub mod shade {
pub const EMPTY: &str = " ";
pub const LIGHT: &str = "";
pub const MEDIUM: &str = "";
pub const DARK: &str = "";
pub const FULL: &str = "";
}
#[cfg(test)]
mod tests {
use strum::ParseError;

View File

@@ -1,4 +1,4 @@
use super::{block, line};
use crate::symbols::{block, line};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Set {

View File

@@ -1,25 +1,29 @@
//! Primitives for styled text.
//!
//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish,
//! those strings may be associated to a set of styles. `ratatui` has three ways to represent them:
//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish, those
//! strings may be associated to a set of styles. `ratatui` has three ways to represent them:
//! - A single line string where all graphemes have the same style is represented by a [`Span`].
//! - A single line string where each grapheme may have its own style is represented by [`Line`].
//! - A multiple line string where each grapheme may have its own style is represented by a
//! [`Text`].
//!
//! These types form a hierarchy: [`Line`] is a collection of [`Span`] and each line of [`Text`]
//! is a [`Line`].
//! These types form a hierarchy: [`Line`] is a collection of [`Span`] and each line of [`Text`] is
//! a [`Line`].
//!
//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is
//! supported for their properties. Moreover, `ratatui` provides convenient `From` implementations
//! so that you can start by using simple `String` or `&str` and then promote them to the previous
//! primitives when you need additional styling capabilities.
//!
//! For example, for the [`crate::widgets::Block`] widget, all the following calls are valid to set
//! its `title` property (which is a [`Line`] under the hood):
//! For example, for the `Block` widget, all the following calls are valid to set its `title`
//! property (which is a [`Line`] under the hood):
//!
//! ```rust
//! use ratatui::{prelude::*, widgets::*};
//! ```rust,ignore
//! use ratatui_core::{
//! style::{Color, Style},
//! text::{Line, Span},
//! widgets::Block,
//! };
//!
//! // A simple string with no styling.
//! // Converted to Line(vec![

View File

@@ -1,4 +1,4 @@
use crate::{prelude::*, style::Styled};
use crate::style::{Style, Styled};
const NBSP: &str = "\u{00a0}";
const ZWSP: &str = "\u{200b}";
@@ -19,6 +19,8 @@ impl<'a> StyledGrapheme<'a> {
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// [`Color`]: crate::style::Color
pub fn new<S: Into<Style>>(symbol: &'a str, style: S) -> Self {
Self {
symbol,
@@ -26,7 +28,7 @@ impl<'a> StyledGrapheme<'a> {
}
}
pub(crate) fn is_whitespace(&self) -> bool {
pub fn is_whitespace(&self) -> bool {
let symbol = self.symbol;
symbol == ZWSP || symbol.chars().all(char::is_whitespace) && symbol != NBSP
}
@@ -48,6 +50,7 @@ impl<'a> Styled for StyledGrapheme<'a> {
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Stylize;
#[test]
fn new() {

293
src/text/line.rs → ratatui-core/src/text/line.rs Normal file → Executable file
View File

@@ -4,7 +4,13 @@ use std::{borrow::Cow, fmt};
use unicode_truncate::UnicodeTruncateStr;
use crate::{prelude::*, style::Styled, text::StyledGrapheme};
use crate::{
buffer::Buffer,
layout::{Alignment, Rect},
style::{Style, Styled},
text::{Span, StyledGrapheme, Text},
widgets::Widget,
};
/// A line of text, consisting of one or more [`Span`]s.
///
@@ -69,7 +75,10 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// [`Style`].
///
/// ```rust
/// use ratatui::prelude::*;
/// use ratatui_core::{
/// style::{Color, Modifier, Style, Stylize},
/// text::{Line, Span},
/// };
///
/// let style = Style::new().yellow();
/// let line = Line::raw("Hello, world!").style(style);
@@ -93,7 +102,11 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// methods of the [`Stylize`] trait.
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::{
/// style::{Color, Modifier, Style, Stylize},
/// text::Line,
/// };
///
/// let line = Line::from("Hello world!").style(Style::new().yellow().italic());
/// let line = Line::from("Hello world!").style(Color::Yellow);
/// let line = Line::from("Hello world!").style((Color::Yellow, Color::Black));
@@ -108,7 +121,8 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// ignored and the line is truncated.
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::{layout::Alignment, text::Line};
///
/// let line = Line::from("Hello world!").alignment(Alignment::Right);
/// let line = Line::from("Hello world!").centered();
/// let line = Line::from("Hello world!").left_aligned();
@@ -120,26 +134,44 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// `Line` implements the [`Widget`] trait, which means it can be rendered to a [`Buffer`].
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::{
/// buffer::Buffer,
/// layout::Rect,
/// style::{Style, Stylize},
/// text::Line,
/// widgets::Widget,
/// };
///
/// # fn render(area: Rect, buf: &mut Buffer) {
/// // in another widget's render method
/// let line = Line::from("Hello world!").style(Style::new().yellow().italic());
/// line.render(area, buf);
/// # }
/// ```
///
/// Or you can use the `render_widget` method on the `Frame` in a `Terminal::draw` closure.
///
/// ```rust,ignore
/// # use ratatui::{Frame, layout::Rect, text::Line};
/// # fn draw(frame: &mut Frame, area: Rect) {
/// // in a terminal.draw closure
/// let line = Line::from("Hello world!").style(Style::new().yellow().italic());
/// let line = Line::from("Hello world!");
/// frame.render_widget(line, area);
/// # }
/// ```
/// ## Rendering Lines with a Paragraph widget
///
/// Usually apps will use the [`Paragraph`] widget instead of rendering a [`Line`] directly as it
/// Usually apps will use the `Paragraph` widget instead of rendering a [`Line`] directly as it
/// provides more functionality.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// ```rust,ignore
/// use ratatui::{
/// buffer::Buffer,
/// layout::Rect,
/// style::Stylize,
/// text::Line,
/// widgets::{Paragraph, Widget, Wrap},
/// };
///
/// # fn render(area: Rect, buf: &mut Buffer) {
/// let line = Line::from("Hello world!").yellow().italic();
/// Paragraph::new(line)
@@ -148,17 +180,44 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// # }
/// ```
///
/// [`Paragraph`]: crate::widgets::Paragraph
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
/// [`Stylize`]: crate::style::Stylize
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct Line<'a> {
/// The spans that make up this line of text.
pub spans: Vec<Span<'a>>,
/// The style of this line of text.
pub style: Style,
/// The alignment of this line of text.
pub alignment: Option<Alignment>,
/// The spans that make up this line of text.
pub spans: Vec<Span<'a>>,
}
impl fmt::Debug for Line<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.spans.is_empty() {
f.write_str("Line::default()")?;
} else if self.spans.len() == 1 && self.spans[0].style == Style::default() {
f.write_str(r#"Line::from(""#)?;
f.write_str(&self.spans[0].content)?;
f.write_str(r#"")"#)?;
} else if self.spans.len() == 1 {
f.write_str("Line::from(")?;
self.spans[0].fmt(f)?;
f.write_str(")")?;
} else {
f.write_str("Line::from_iter(")?;
f.debug_list().entries(&self.spans).finish()?;
f.write_str(")")?;
}
self.style.fmt_stylize(f)?;
match self.alignment {
Some(Alignment::Left) => write!(f, ".left_aligned()"),
Some(Alignment::Center) => write!(f, ".centered()"),
Some(Alignment::Right) => write!(f, ".right_aligned()"),
None => Ok(()),
}
}
}
fn cow_to_spans<'a>(content: impl Into<Cow<'a, str>>) -> Vec<Span<'a>> {
@@ -182,8 +241,10 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # use std::borrow::Cow;
/// use std::borrow::Cow;
///
/// use ratatui_core::text::Line;
///
/// Line::raw("test content");
/// Line::raw(String::from("test content"));
/// Line::raw(Cow::from("test content"));
@@ -211,13 +272,20 @@ impl<'a> Line<'a> {
/// Any newlines in the content are removed.
///
/// ```rust
/// # use ratatui::prelude::*;
/// # use std::borrow::Cow;
/// use std::borrow::Cow;
///
/// use ratatui_core::{
/// style::{Style, Stylize},
/// text::Line,
/// };
///
/// let style = Style::new().yellow().italic();
/// Line::styled("My text", style);
/// Line::styled(String::from("My text"), style);
/// Line::styled(Cow::from("test content"), style);
/// ```
///
/// [`Color`]: crate::style::Color
pub fn styled<T, S>(content: T, style: S) -> Self
where
T: Into<Cow<'a, str>>,
@@ -238,7 +306,8 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::{style::Stylize, text::Line};
///
/// let line = Line::default().spans(vec!["Hello".blue(), " world!".green()]);
/// let line = Line::default().spans([1, 2, 3].iter().map(|i| format!("Item {}", i)));
/// ```
@@ -265,9 +334,15 @@ impl<'a> Line<'a> {
///
/// # Examples
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::{
/// style::{Style, Stylize},
/// text::Line,
/// };
///
/// let mut line = Line::from("foo").style(Style::new().red());
/// ```
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
@@ -283,7 +358,8 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::{layout::Alignment, text::Line};
///
/// let mut line = Line::from("Hi, what's up?");
/// assert_eq!(None, line.alignment);
/// assert_eq!(
@@ -308,7 +384,8 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::text::Line;
///
/// let line = Line::from("Hi, what's up?").left_aligned();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -325,7 +402,8 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::text::Line;
///
/// let line = Line::from("Hi, what's up?").centered();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -342,7 +420,8 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::text::Line;
///
/// let line = Line::from("Hi, what's up?").right_aligned();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -355,7 +434,8 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::{style::Stylize, text::Line};
///
/// let line = Line::from(vec!["Hello".blue(), " world!".green()]);
/// assert_eq!(12, line.width());
/// ```
@@ -376,7 +456,10 @@ impl<'a> Line<'a> {
/// ```rust
/// use std::iter::Iterator;
///
/// use ratatui::{prelude::*, text::StyledGrapheme};
/// use ratatui_core::{
/// style::{Color, Style},
/// text::{Line, StyledGrapheme},
/// };
///
/// let line = Line::styled("Text", Style::default().fg(Color::Yellow));
/// let style = Style::default().fg(Color::Green).bg(Color::Black);
@@ -391,6 +474,8 @@ impl<'a> Line<'a> {
/// ]
/// );
/// ```
///
/// [`Color`]: crate::style::Color
pub fn styled_graphemes<S: Into<Style>>(
&'a self,
base_style: S,
@@ -415,13 +500,19 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::{
/// style::{Color, Modifier},
/// text::Line,
/// };
///
/// let line = Line::styled("My text", Modifier::ITALIC);
///
/// let styled_line = Line::styled("My text", (Color::Yellow, Modifier::ITALIC));
///
/// assert_eq!(styled_line, line.patch_style(Color::Yellow));
/// ```
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = self.style.patch(style);
@@ -437,8 +528,12 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// # let style = Style::default().yellow();
/// use ratatui_core::{
/// style::{Style, Stylize},
/// text::Line,
/// };
///
/// let line = Line::styled("My text", style);
///
/// assert_eq!(Style::reset(), line.reset_style().style);
@@ -466,7 +561,8 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::text::{Line, Span};
///
/// let mut line = Line::from("Hello, ");
/// line.push_span(Span::raw("world!"));
/// line.push_span(" How are you?");
@@ -515,6 +611,12 @@ impl<'a> From<&'a str> for Line<'a> {
}
}
impl<'a> From<Cow<'a, str>> for Line<'a> {
fn from(s: Cow<'a, str>) -> Self {
Self::raw(s)
}
}
impl<'a> From<Vec<Span<'a>>> for Line<'a> {
fn from(spans: Vec<Span<'a>>) -> Self {
Self {
@@ -581,12 +683,25 @@ impl<'a> Extend<Span<'a>> for Line<'a> {
impl Widget for Line<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
Widget::render(&self, area, buf);
}
}
impl WidgetRef for Line<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
impl Widget for &Line<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_with_alignment(area, buf, None);
}
}
impl Line<'_> {
/// An internal implementation method for `Widget::render` that allows the parent widget to
/// define a default alignment, to be used if `Line::alignment` is `None`.
pub(crate) fn render_with_alignment(
&self,
area: Rect,
buf: &mut Buffer,
parent_alignment: Option<Alignment>,
) {
let area = area.intersection(buf.area);
if area.is_empty() {
return;
@@ -599,10 +714,12 @@ impl WidgetRef for Line<'_> {
buf.set_style(area, self.style);
let alignment = self.alignment.or(parent_alignment);
let area_width = usize::from(area.width);
let can_render_complete_line = line_width <= area_width;
if can_render_complete_line {
let indent_width = match self.alignment {
let indent_width = match alignment {
Some(Alignment::Center) => (area_width.saturating_sub(line_width)) / 2,
Some(Alignment::Right) => area_width.saturating_sub(line_width),
Some(Alignment::Left) | None => 0,
@@ -613,7 +730,7 @@ impl WidgetRef for Line<'_> {
} else {
// There is not enough space to render the whole line. As the right side is truncated by
// the area width, only truncate the left.
let skip_width = match self.alignment {
let skip_width = match alignment {
Some(Alignment::Center) => (line_width.saturating_sub(area_width)) / 2,
Some(Alignment::Right) => line_width.saturating_sub(area_width),
Some(Alignment::Left) | None => 0,
@@ -630,7 +747,7 @@ fn render_spans(spans: &[Span], mut area: Rect, buf: &mut Buffer, span_skip_widt
if area.is_empty() {
break;
}
span.render_ref(area, buf);
span.render(area, buf);
let span_width = u16::try_from(span_width).unwrap_or(u16::MAX);
area = area.indent_x(span_width);
}
@@ -732,6 +849,7 @@ mod tests {
use rstest::{fixture, rstest};
use super::*;
use crate::style::{Color, Modifier, Stylize};
#[fixture]
fn small_buf() -> Buffer {
@@ -741,11 +859,11 @@ mod tests {
#[test]
fn raw_str() {
let line = Line::raw("test content");
assert_eq!(line.spans, vec![Span::raw("test content")]);
assert_eq!(line.spans, [Span::raw("test content")]);
assert_eq!(line.alignment, None);
let line = Line::raw("a\nb");
assert_eq!(line.spans, vec![Span::raw("a"), Span::raw("b")]);
assert_eq!(line.spans, [Span::raw("a"), Span::raw("b")]);
assert_eq!(line.alignment, None);
}
@@ -754,7 +872,7 @@ mod tests {
let style = Style::new().yellow();
let content = "Hello, world!";
let line = Line::styled(content, style);
assert_eq!(line.spans, vec![Span::raw(content)]);
assert_eq!(line.spans, [Span::raw(content)]);
assert_eq!(line.style, style);
}
@@ -763,7 +881,7 @@ mod tests {
let style = Style::new().yellow();
let content = String::from("Hello, world!");
let line = Line::styled(content.clone(), style);
assert_eq!(line.spans, vec![Span::raw(content)]);
assert_eq!(line.spans, [Span::raw(content)]);
assert_eq!(line.style, style);
}
@@ -772,7 +890,7 @@ mod tests {
let style = Style::new().yellow();
let content = Cow::from("Hello, world!");
let line = Line::styled(content.clone(), style);
assert_eq!(line.spans, vec![Span::raw(content)]);
assert_eq!(line.spans, [Span::raw(content)]);
assert_eq!(line.style, style);
}
@@ -861,28 +979,28 @@ mod tests {
fn from_string() {
let s = String::from("Hello, world!");
let line = Line::from(s);
assert_eq!(line.spans, vec![Span::from("Hello, world!")]);
assert_eq!(line.spans, [Span::from("Hello, world!")]);
let s = String::from("Hello\nworld!");
let line = Line::from(s);
assert_eq!(line.spans, vec![Span::from("Hello"), Span::from("world!")]);
assert_eq!(line.spans, [Span::from("Hello"), Span::from("world!")]);
}
#[test]
fn from_str() {
let s = "Hello, world!";
let line = Line::from(s);
assert_eq!(line.spans, vec![Span::from("Hello, world!")]);
assert_eq!(line.spans, [Span::from("Hello, world!")]);
let s = "Hello\nworld!";
let line = Line::from(s);
assert_eq!(line.spans, vec![Span::from("Hello"), Span::from("world!")]);
assert_eq!(line.spans, [Span::from("Hello"), Span::from("world!")]);
}
#[test]
fn to_line() {
let line = 42.to_line();
assert_eq!(vec![Span::from("42")], line.spans);
assert_eq!(line.spans, [Span::from("42")]);
}
#[test]
@@ -925,7 +1043,7 @@ mod tests {
fn from_span() {
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));
let line = Line::from(span.clone());
assert_eq!(line.spans, vec![span],);
assert_eq!(line.spans, [span]);
}
#[test]
@@ -969,14 +1087,14 @@ mod tests {
#[test]
fn extend() {
let mut line = Line::from("Hello, ");
line.extend(vec![Span::raw("world!")]);
assert_eq!(line.spans, vec![Span::raw("Hello, "), Span::raw("world!")]);
line.extend([Span::raw("world!")]);
assert_eq!(line.spans, [Span::raw("Hello, "), Span::raw("world!")]);
let mut line = Line::from("Hello, ");
line.extend(vec![Span::raw("world! "), Span::raw("How are you?")]);
line.extend([Span::raw("world! "), Span::raw("How are you?")]);
assert_eq!(
line.spans,
vec![
[
Span::raw("Hello, "),
Span::raw("world! "),
Span::raw("How are you?")
@@ -1193,7 +1311,7 @@ mod tests {
assert_eq!(buf, Buffer::with_lines(["lo wo"]));
}
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
/// Part of a regression test for <https://github.com/ratatui/ratatui/issues/1032> which
/// found panics with truncating lines that contained multi-byte characters.
#[test]
fn regression_1032() {
@@ -1201,7 +1319,7 @@ mod tests {
"🦀 RFC8628 OAuth 2.0 Device Authorization GrantでCLIからGithubのaccess tokenを取得する"
);
let mut buf = Buffer::empty(Rect::new(0, 0, 83, 1));
line.render_ref(buf.area, &mut buf);
line.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([
"🦀 RFC8628 OAuth 2.0 Device Authorization GrantでCLIからGithubのaccess tokenを取得 "
]));
@@ -1209,7 +1327,7 @@ mod tests {
/// Documentary test to highlight the crab emoji width / length discrepancy
///
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
/// Part of a regression test for <https://github.com/ratatui/ratatui/issues/1032> which
/// found panics with truncating lines that contained multi-byte characters.
#[test]
fn crab_emoji_width() {
@@ -1220,7 +1338,7 @@ mod tests {
assert_eq!(crab.width(), 2); // display width
}
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
/// Part of a regression test for <https://github.com/ratatui/ratatui/issues/1032> which
/// found panics with truncating lines that contained multi-byte characters.
#[rstest]
#[case::left_4(Alignment::Left, 4, "1234")]
@@ -1238,11 +1356,11 @@ mod tests {
) {
let line = Line::from("1234🦀7890").alignment(alignment);
let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
line.render_ref(buf.area, &mut buf);
line.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected]));
}
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
/// Part of a regression test for <https://github.com/ratatui/ratatui/issues/1032> which
/// found panics with truncating lines that contained multi-byte characters.
///
/// centering is tricky because there's an ambiguity about whether to take one more char
@@ -1291,7 +1409,7 @@ mod tests {
};
let line = Line::from(value).centered();
let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
line.render_ref(buf.area, &mut buf);
line.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected]));
}
@@ -1309,7 +1427,7 @@ mod tests {
// Fill buffer with stuff to ensure the output is indeed padded
let mut buf = Buffer::filled(Rect::new(0, 0, 10, 1), Cell::new("X"));
let area = Rect::new(2, 0, 6, 1);
line.render_ref(area, &mut buf);
line.render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected]));
}
@@ -1327,11 +1445,11 @@ mod tests {
let area = Rect::new(0, 0, buf_width, 1);
// Fill buffer with stuff to ensure the output is indeed padded
let mut buf = Buffer::filled(area, Cell::new("X"));
line.render_ref(buf.area, &mut buf);
line.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected]));
}
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
/// Part of a regression test for <https://github.com/ratatui/ratatui/issues/1032> which
/// found panics with truncating lines that contained multi-byte characters.
///
/// Flag emoji are actually two independent characters, so they can be truncated in the
@@ -1345,7 +1463,7 @@ mod tests {
assert_eq!(str.width(), 6); // flag is 2 display width
}
/// Part of a regression test for <https://github.com/ratatui-org/ratatui/issues/1032> which
/// Part of a regression test for <https://github.com/ratatui/ratatui/issues/1032> which
/// found panics with truncating lines that contained multi-byte characters.
#[rstest]
#[case::flag_1(1, " ")]
@@ -1358,7 +1476,7 @@ mod tests {
fn render_truncates_flag(#[case] buf_width: u16, #[case] expected: &str) {
let line = Line::from("🇺🇸1234");
let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
line.render_ref(buf.area, &mut buf);
line.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected]));
}
@@ -1382,7 +1500,7 @@ mod tests {
assert!(line.width() >= min_width);
let mut buf = Buffer::empty(Rect::new(0, 0, 32, 1));
line.render_ref(buf.area, &mut buf);
line.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected]));
}
@@ -1406,7 +1524,7 @@ mod tests {
assert!(line.width() >= min_width);
let mut buf = Buffer::empty(Rect::new(0, 0, 32, 1));
line.render_ref(buf.area, &mut buf);
line.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected]));
}
@@ -1505,4 +1623,49 @@ mod tests {
assert_eq!(result, "Hello world!");
}
}
#[rstest]
#[case::empty(Line::default(), "Line::default()")]
#[case::raw(Line::raw("Hello, world!"), r#"Line::from("Hello, world!")"#)]
#[case::styled(
Line::styled("Hello, world!", Color::Yellow),
r#"Line::from("Hello, world!").yellow()"#
)]
#[case::styled_complex(
Line::from(String::from("Hello, world!")).green().on_blue().bold().italic().not_dim(),
r#"Line::from("Hello, world!").green().on_blue().bold().italic().not_dim()"#
)]
#[case::styled_span(
Line::from(Span::styled("Hello, world!", Color::Yellow)),
r#"Line::from(Span::from("Hello, world!").yellow())"#
)]
#[case::styled_line_and_span(
Line::from(vec![
Span::styled("Hello", Color::Yellow),
Span::styled(" world!", Color::Green),
]).italic(),
r#"Line::from_iter([Span::from("Hello").yellow(), Span::from(" world!").green()]).italic()"#
)]
#[case::spans_vec(
Line::from(vec![
Span::styled("Hello", Color::Blue),
Span::styled(" world!", Color::Green),
]),
r#"Line::from_iter([Span::from("Hello").blue(), Span::from(" world!").green()])"#,
)]
#[case::left_aligned(
Line::from("Hello, world!").left_aligned(),
r#"Line::from("Hello, world!").left_aligned()"#
)]
#[case::centered(
Line::from("Hello, world!").centered(),
r#"Line::from("Hello, world!").centered()"#
)]
#[case::right_aligned(
Line::from("Hello, world!").right_aligned(),
r#"Line::from("Hello, world!").right_aligned()"#
)]
fn debug(#[case] line: Line, #[case] expected: &str) {
assert_eq!(format!("{line:?}"), expected);
}
}

View File

@@ -1,21 +1,26 @@
use std::{borrow::Cow, fmt};
use super::Text;
use crate::text::Text;
/// A wrapper around a string that is masked when displayed.
///
/// The masked string is displayed as a series of the same character.
/// This might be used to display a password field or similar secure data.
/// The masked string is displayed as a series of the same character. This might be used to display
/// a password field or similar secure data.
///
/// # Examples
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui_core::{
/// buffer::Buffer,
/// layout::Rect,
/// text::{Masked, Text},
/// widgets::Widget,
/// };
///
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
/// let password = Masked::new("12345", 'x');
///
/// Paragraph::new(password).render(buffer.area, &mut buffer);
/// Text::from(password).render(buffer.area, &mut buffer);
/// assert_eq!(buffer, Buffer::with_lines(["xxxxx"]));
/// ```
#[derive(Default, Clone, Eq, PartialEq, Hash)]
@@ -125,10 +130,10 @@ mod tests {
let masked = Masked::new("12345", 'x');
let text: Text = (&masked).into();
assert_eq!(text.lines, vec![Line::from("xxxxx")]);
assert_eq!(text.lines, [Line::from("xxxxx")]);
let text: Text = masked.into();
assert_eq!(text.lines, vec![Line::from("xxxxx")]);
assert_eq!(text.lines, [Line::from("xxxxx")]);
}
#[test]

View File

@@ -3,7 +3,13 @@ use std::{borrow::Cow, fmt};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::{prelude::*, style::Styled, text::StyledGrapheme};
use crate::{
buffer::Buffer,
layout::Rect,
style::{Style, Styled},
text::{Line, StyledGrapheme},
widgets::Widget,
};
/// Represents a part of a line that is contiguous and where all characters share the same style.
///
@@ -36,7 +42,7 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// any type convertible to [`Cow<str>`].
///
/// ```rust
/// use ratatui::prelude::*;
/// use ratatui_core::text::Span;
///
/// let span = Span::raw("test content");
/// let span = Span::raw(String::from("test content"));
@@ -50,7 +56,10 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// the [`Stylize`] trait.
///
/// ```rust
/// use ratatui::prelude::*;
/// use ratatui_core::{
/// style::{Style, Stylize},
/// text::Span,
/// };
///
/// let span = Span::styled("test content", Style::new().green());
/// let span = Span::styled(String::from("test content"), Style::new().green());
@@ -64,7 +73,7 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// defined in the [`Stylize`] trait.
///
/// ```rust
/// use ratatui::prelude::*;
/// use ratatui_core::{style::Stylize, text::Span};
///
/// let span = Span::raw("test content").green().on_yellow().italic();
/// let span = Span::raw(String::from("test content"))
@@ -73,27 +82,40 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// .italic();
/// ```
///
/// `Span` implements the [`Widget`] trait, which allows it to be rendered to a [`Buffer`]. Usually
/// apps will use the [`Paragraph`] widget instead of rendering `Span` directly, as it handles text
/// `Span` implements the [`Widget`] trait, which allows it to be rendered to a [`Buffer`]. Often
/// apps will use the `Paragraph` widget instead of rendering `Span` directly, as it handles text
/// wrapping and alignment for you.
///
/// ```rust
/// use ratatui::prelude::*;
/// ```rust,ignore
/// use ratatui::{style::Stylize, Frame};
///
/// # fn render_frame(frame: &mut Frame) {
/// frame.render_widget("test content".green().on_yellow().italic(), frame.size());
/// frame.render_widget("test content".green().on_yellow().italic(), frame.area());
/// # }
/// ```
/// [`Line`]: crate::text::Line
/// [`Paragraph`]: crate::widgets::Paragraph
/// [`Stylize`]: crate::style::Stylize
/// [`Cow<str>`]: std::borrow::Cow
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct Span<'a> {
/// The content of the span as a Clone-on-write string.
pub content: Cow<'a, str>,
/// The style of the span.
pub style: Style,
/// The content of the span as a Clone-on-write string.
pub content: Cow<'a, str>,
}
impl fmt::Debug for Span<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.content.is_empty() {
write!(f, "Span::default()")?;
} else {
write!(f, "Span::from({:?})", self.content)?;
}
if self.style != Style::default() {
self.style.fmt_stylize(f)?;
}
Ok(())
}
}
impl<'a> Span<'a> {
@@ -102,7 +124,8 @@ impl<'a> Span<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::text::Span;
///
/// Span::raw("test content");
/// Span::raw(String::from("test content"));
/// ```
@@ -127,11 +150,17 @@ impl<'a> Span<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::{
/// style::{Style, Stylize},
/// text::Span,
/// };
///
/// let style = Style::new().yellow().on_green().italic();
/// Span::styled("test content", style);
/// Span::styled(String::from("test content"), style);
/// ```
///
/// [`Color`]: crate::style::Color
pub fn styled<T, S>(content: T, style: S) -> Self
where
T: Into<Cow<'a, str>>,
@@ -153,7 +182,8 @@ impl<'a> Span<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::text::Span;
///
/// let mut span = Span::default().content("content");
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -178,9 +208,15 @@ impl<'a> Span<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::{
/// style::{Style, Stylize},
/// text::Span,
/// };
///
/// let mut span = Span::default().style(Style::new().green());
/// ```
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
@@ -197,11 +233,17 @@ impl<'a> Span<'a> {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::{
/// style::{Style, Stylize},
/// text::Span,
/// };
///
/// let span = Span::styled("test content", Style::new().green().italic())
/// .patch_style(Style::new().red().on_yellow().bold());
/// assert_eq!(span.style, Style::new().red().on_yellow().italic().bold());
/// ```
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = self.style.patch(style);
@@ -217,7 +259,11 @@ impl<'a> Span<'a> {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::{
/// style::{Style, Stylize},
/// text::Span,
/// };
///
/// let span = Span::styled(
/// "Test Content",
/// Style::new().dark_gray().on_yellow().italic(),
@@ -248,7 +294,10 @@ impl<'a> Span<'a> {
/// ```rust
/// use std::iter::Iterator;
///
/// use ratatui::{prelude::*, text::StyledGrapheme};
/// use ratatui_core::{
/// style::{Style, Stylize},
/// text::{Span, StyledGrapheme},
/// };
///
/// let span = Span::styled("Test", Style::new().green().italic());
/// let style = Style::new().red().on_yellow();
@@ -263,6 +312,8 @@ impl<'a> Span<'a> {
/// ],
/// );
/// ```
///
/// [`Color`]: crate::style::Color
pub fn styled_graphemes<S: Into<Style>>(
&'a self,
base_style: S,
@@ -280,7 +331,8 @@ impl<'a> Span<'a> {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::style::Stylize;
///
/// let line = "Test Content".green().italic().into_left_aligned_line();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -299,7 +351,8 @@ impl<'a> Span<'a> {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::style::Stylize;
///
/// let line = "Test Content".green().italic().into_centered_line();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -318,7 +371,8 @@ impl<'a> Span<'a> {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::style::Stylize;
///
/// let line = "Test Content".green().italic().into_right_aligned_line();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -364,12 +418,12 @@ impl<'a> Styled for Span<'a> {
impl Widget for Span<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
Widget::render(&self, area, buf);
}
}
impl WidgetRef for Span<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
impl Widget for &Span<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let area = area.intersection(buf.area);
if area.is_empty() {
return;
@@ -452,10 +506,10 @@ impl fmt::Display for Span<'_> {
#[cfg(test)]
mod tests {
use buffer::Cell;
use rstest::fixture;
use rstest::{fixture, rstest};
use super::*;
use crate::{buffer::Cell, layout::Alignment, style::Stylize};
#[fixture]
fn small_buf() -> Buffer {
@@ -569,7 +623,7 @@ mod tests {
assert_eq!(Span::raw("").width(), 0);
assert_eq!(Span::raw("test").width(), 4);
assert_eq!(Span::raw("test content").width(), 12);
// Needs reconsideration: https://github.com/ratatui-org/ratatui/issues/1271
// Needs reconsideration: https://github.com/ratatui/ratatui/issues/1271
assert_eq!(Span::raw("test\ncontent").width(), 12);
}
@@ -786,7 +840,7 @@ mod tests {
}
}
/// Regression test for <https://github.com/ratatui-org/ratatui/issues/1160> One line contains
/// Regression test for <https://github.com/ratatui/ratatui/issues/1160> One line contains
/// some Unicode Left-Right-Marks (U+200E)
///
/// The issue was that a zero-width character at the end of the buffer causes the buffer bounds
@@ -831,4 +885,16 @@ mod tests {
Line::from(vec![Span::raw("test"), Span::raw("content")])
);
}
#[rstest]
#[case::default(Span::default(), "Span::default()")]
#[case::raw(Span::raw("test"), r#"Span::from("test")"#)]
#[case::styled(Span::styled("test", Style::new().green()), r#"Span::from("test").green()"#)]
#[case::styled_italic(
Span::styled("test", Style::new().green().italic()),
r#"Span::from("test").green().italic()"#
)]
fn debug(#[case] span: Span, #[case] expected: &str) {
assert_eq!(format!("{span:?}"), expected);
}
}

290
src/text/text.rs → ratatui-core/src/text/text.rs Normal file → Executable file
View File

@@ -1,9 +1,13 @@
#![warn(missing_docs)]
use std::{borrow::Cow, fmt};
use itertools::{Itertools, Position};
use crate::{prelude::*, style::Styled};
use crate::{
buffer::Buffer,
layout::{Alignment, Rect},
style::{Style, Styled},
text::{Line, Span},
widgets::Widget,
};
/// A string split over one or more lines.
///
@@ -64,7 +68,10 @@ use crate::{prelude::*, style::Styled};
/// ```rust
/// use std::{borrow::Cow, iter};
///
/// use ratatui::prelude::*;
/// use ratatui_core::{
/// style::{Color, Modifier, Style, Stylize},
/// text::{Line, Span, Text},
/// };
///
/// let style = Style::new().yellow().italic();
/// let text = Text::raw("The first line\nThe second line").style(style);
@@ -101,7 +108,11 @@ use crate::{prelude::*, style::Styled};
/// [`Stylize`] trait.
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::{
/// style::{Color, Modifier, Style, Stylize},
/// text::{Line, Text},
/// };
///
/// let text = Text::from("The first line\nThe second line").style(Style::new().yellow().italic());
/// let text = Text::from("The first line\nThe second line")
/// .yellow()
@@ -118,7 +129,11 @@ use crate::{prelude::*, style::Styled};
/// Lines composing the text can also be individually aligned with [`Line::alignment`].
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::{
/// layout::Alignment,
/// text::{Line, Text},
/// };
///
/// let text = Text::from("The first line\nThe second line").alignment(Alignment::Right);
/// let text = Text::from("The first line\nThe second line").right_aligned();
/// let text = Text::from(vec![
@@ -131,17 +146,23 @@ use crate::{prelude::*, style::Styled};
///
/// ## Rendering Text
/// `Text` implements the [`Widget`] trait, which means it can be rendered to a [`Buffer`] or to a
/// [`Frame`].
/// `Frame`.
///
/// ```rust
/// # use ratatui::prelude::*;
/// # use ratatui_core::{buffer::Buffer, layout::Rect};
/// use ratatui_core::{text::Text, widgets::Widget};
///
/// // within another widget's `render` method:
/// # fn render(area: Rect, buf: &mut Buffer) {
/// let text = Text::from("The first line\nThe second line");
/// text.render(area, buf);
/// # }
/// ```
///
/// // within a terminal.draw closure:
/// Or you can use the `render_widget` method on a `Frame` within a `Terminal::draw` closure.
///
/// ```rust,ignore
/// # use ratatui::{Frame, layout::Rect, text::Text};
/// # fn draw(frame: &mut Frame, area: Rect) {
/// let text = Text::from("The first line\nThe second line");
/// frame.render_widget(text, area);
@@ -150,11 +171,17 @@ use crate::{prelude::*, style::Styled};
///
/// ## Rendering Text with a Paragraph Widget
///
/// Usually apps will use the [`Paragraph`] widget instead of rendering a `Text` directly as it
/// Usually apps will use the `Paragraph` widget instead of rendering a `Text` directly as it
/// provides more functionality.
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// ```rust,ignore
/// use ratatui::{
/// buffer::Buffer,
/// layout::Rect,
/// text::Text,
/// widgets::{Paragraph, Widget, Wrap},
/// };
///
/// # fn render(area: Rect, buf: &mut Buffer) {
/// let text = Text::from("The first line\nThe second line");
/// let paragraph = Paragraph::new(text)
@@ -165,14 +192,37 @@ use crate::{prelude::*, style::Styled};
/// ```
///
/// [`Paragraph`]: crate::widgets::Paragraph
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
/// [`Stylize`]: crate::style::Stylize
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct Text<'a> {
/// The lines that make up this piece of text.
pub lines: Vec<Line<'a>>,
/// The style of this text.
pub style: Style,
/// The alignment of this text.
pub alignment: Option<Alignment>,
/// The style of this text.
pub style: Style,
/// The lines that make up this piece of text.
pub lines: Vec<Line<'a>>,
}
impl fmt::Debug for Text<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.lines.is_empty() {
f.write_str("Text::default()")?;
} else if self.lines.len() == 1 {
write!(f, "Text::from({:?})", self.lines[0])?;
} else {
f.write_str("Text::from_iter(")?;
f.debug_list().entries(self.lines.iter()).finish()?;
f.write_str(")")?;
}
self.style.fmt_stylize(f)?;
match self.alignment {
Some(Alignment::Left) => f.write_str(".left_aligned()")?,
Some(Alignment::Center) => f.write_str(".centered()")?,
Some(Alignment::Right) => f.write_str(".right_aligned()")?,
_ => (),
}
Ok(())
}
}
impl<'a> Text<'a> {
@@ -181,7 +231,8 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::text::Text;
///
/// Text::raw("The first line\nThe second line");
/// Text::raw(String::from("The first line\nThe second line"));
/// ```
@@ -206,13 +257,19 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::{
/// style::{Color, Modifier, Style},
/// text::Text,
/// };
///
/// let style = Style::default()
/// .fg(Color::Yellow)
/// .add_modifier(Modifier::ITALIC);
/// Text::styled("The first line\nThe second line", style);
/// Text::styled(String::from("The first line\nThe second line"), style);
/// ```
///
/// [`Color`]: crate::style::Color
pub fn styled<T, S>(content: T, style: S) -> Self
where
T: Into<Cow<'a, str>>,
@@ -226,7 +283,8 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::text::Text;
///
/// let text = Text::from("The first line\nThe second line");
/// assert_eq!(15, text.width());
/// ```
@@ -239,7 +297,8 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::text::Text;
///
/// let text = Text::from("The first line\nThe second line");
/// assert_eq!(2, text.height());
/// ```
@@ -260,9 +319,15 @@ impl<'a> Text<'a> {
///
/// # Examples
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::{
/// style::{Style, Stylize},
/// text::Text,
/// };
///
/// let mut line = Text::from("foo").style(Style::new().red());
/// ```
///
/// [`Color`]: crate::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
@@ -286,7 +351,11 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::{
/// style::{Color, Modifier},
/// text::Text,
/// };
///
/// let raw_text = Text::styled("The first line\nThe second line", Modifier::ITALIC);
/// let styled_text = Text::styled(
/// String::from("The first line\nThe second line"),
@@ -297,6 +366,9 @@ impl<'a> Text<'a> {
/// let raw_text = raw_text.patch_style(Color::Yellow);
/// assert_eq!(raw_text, styled_text);
/// ```
///
/// [`Color`]: crate::style::Color
/// [`Stylize`]: crate::style::Stylize
#[must_use = "method moves the value of self and returns the modified value"]
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = self.style.patch(style);
@@ -312,7 +384,11 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::{
/// style::{Color, Modifier, Style},
/// text::Text,
/// };
///
/// let text = Text::styled(
/// "The first line\nThe second line",
/// (Color::Yellow, Modifier::ITALIC),
@@ -339,7 +415,8 @@ impl<'a> Text<'a> {
/// Set alignment to the whole text.
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::{layout::Alignment, text::Text};
///
/// let mut text = Text::from("Hi, what's up?");
/// assert_eq!(None, text.alignment);
/// assert_eq!(
@@ -351,7 +428,11 @@ impl<'a> Text<'a> {
/// Set a default alignment and override it on a per line basis.
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::{
/// layout::Alignment,
/// text::{Line, Text},
/// };
///
/// let text = Text::from(vec![
/// Line::from("left").alignment(Alignment::Left),
/// Line::from("default"),
@@ -388,7 +469,8 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::text::Text;
///
/// let text = Text::from("Hi, what's up?").left_aligned();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -407,7 +489,8 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::text::Text;
///
/// let text = Text::from("Hi, what's up?").centered();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -426,7 +509,8 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::text::Text;
///
/// let text = Text::from("Hi, what's up?").right_aligned();
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@@ -452,7 +536,8 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::text::{Line, Span, Text};
///
/// let mut text = Text::from("Hello, world!");
/// text.push_line(Line::from("How are you?"));
/// text.push_line(Span::from("How are you?"));
@@ -470,7 +555,8 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::text::{Span, Text};
///
/// let mut text = Text::from("Hello, world!");
/// text.push_span(Span::from("How are you?"));
/// text.push_span("How are you?");
@@ -632,12 +718,11 @@ impl<T: fmt::Display> ToText for T {
impl fmt::Display for Text<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (position, line) in self.iter().with_position() {
if position == Position::Last {
write!(f, "{line}")?;
} else {
if let Some((last, rest)) = self.lines.split_last() {
for line in rest {
writeln!(f, "{line}")?;
}
write!(f, "{last}")?;
}
Ok(())
}
@@ -645,31 +730,16 @@ impl fmt::Display for Text<'_> {
impl Widget for Text<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
Widget::render(&self, area, buf)
}
}
impl WidgetRef for Text<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
impl Widget for &Text<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let area = area.intersection(buf.area);
buf.set_style(area, self.style);
for (line, row) in self.iter().zip(area.rows()) {
let line_width = line.width() as u16;
let x_offset = match (self.alignment, line.alignment) {
(Some(Alignment::Center), None) => area.width.saturating_sub(line_width) / 2,
(Some(Alignment::Right), None) => area.width.saturating_sub(line_width),
_ => 0,
};
let line_area = Rect {
x: area.x + x_offset,
y: row.y,
width: area.width - x_offset,
height: 1,
};
line.render(line_area, buf);
for (line, line_area) in self.iter().zip(area.rows()) {
line.render_with_alignment(line_area, buf, self.alignment);
}
}
}
@@ -693,6 +763,7 @@ mod tests {
use rstest::{fixture, rstest};
use super::*;
use crate::style::{Color, Modifier, Stylize};
#[fixture]
fn small_buf() -> Buffer {
@@ -794,7 +865,7 @@ mod tests {
#[test]
fn from_line() {
let text = Text::from(Line::from("The first line"));
assert_eq!(text.lines, vec![Line::from("The first line")]);
assert_eq!(text.lines, [Line::from("The first line")]);
}
#[rstest]
@@ -945,11 +1016,12 @@ mod tests {
);
}
#[test]
fn display_raw_text() {
let text = Text::raw("The first line\nThe second line");
assert_eq!(format!("{text}"), "The first line\nThe second line");
#[rstest]
#[case::one_line("The first line")]
#[case::multiple_lines("The first line\nThe second line")]
fn display_raw_text(#[case] value: &str) {
let text = Text::raw(value);
assert_eq!(format!("{text}"), value);
}
#[test]
@@ -1041,7 +1113,7 @@ mod tests {
fn push_line_empty() {
let mut text = Text::default();
text.push_line(Line::from("Hello, world!"));
assert_eq!(text.lines, vec![Line::from("Hello, world!")]);
assert_eq!(text.lines, [Line::from("Hello, world!")]);
}
#[test]
@@ -1063,7 +1135,7 @@ mod tests {
fn push_span_empty() {
let mut text = Text::default();
text.push_span(Span::raw("Hello, world!"));
assert_eq!(text.lines, vec![Line::from(Span::raw("Hello, world!"))],);
assert_eq!(text.lines, [Line::from(Span::raw("Hello, world!"))]);
}
mod widget {
@@ -1112,6 +1184,33 @@ mod tests {
assert_eq!(buf, Buffer::with_lines([" foo "]));
}
#[test]
fn render_right_aligned_with_truncation() {
let text = Text::from("123456789").alignment(Alignment::Right);
let area = Rect::new(0, 0, 5, 1);
let mut buf = Buffer::empty(area);
text.render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["56789"]));
}
#[test]
fn render_centered_odd_with_truncation() {
let text = Text::from("123456789").alignment(Alignment::Center);
let area = Rect::new(0, 0, 5, 1);
let mut buf = Buffer::empty(area);
text.render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["34567"]));
}
#[test]
fn render_centered_even_with_truncation() {
let text = Text::from("123456789").alignment(Alignment::Center);
let area = Rect::new(0, 0, 6, 1);
let mut buf = Buffer::empty(area);
text.render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["234567"]));
}
#[test]
fn render_one_line_right() {
let text = Text::from(vec![
@@ -1234,4 +1333,69 @@ mod tests {
assert_eq!(result, "Hello world!");
}
}
#[rstest]
#[case::default(Text::default(), "Text::default()")]
// TODO jm: these could be improved to inspect the line / span if there's only one. e.g.
// Text::from("Hello, world!") and Text::from("Hello, world!".blue()) but the current
// implementation is good enough for now.
#[case::raw(
Text::raw("Hello, world!"),
r#"Text::from(Line::from("Hello, world!"))"#
)]
#[case::styled(
Text::styled("Hello, world!", Color::Yellow),
r#"Text::from(Line::from("Hello, world!")).yellow()"#
)]
#[case::complex_styled(
Text::from("Hello, world!").yellow().on_blue().bold().italic().not_dim().not_hidden(),
r#"Text::from(Line::from("Hello, world!")).yellow().on_blue().bold().italic().not_dim().not_hidden()"#
)]
#[case::alignment(
Text::from("Hello, world!").centered(),
r#"Text::from(Line::from("Hello, world!")).centered()"#
)]
#[case::styled_alignment(
Text::styled("Hello, world!", Color::Yellow).centered(),
r#"Text::from(Line::from("Hello, world!")).yellow().centered()"#
)]
#[case::multiple_lines(
Text::from(vec![
Line::from("Hello, world!"),
Line::from("How are you?")
]),
r#"Text::from_iter([Line::from("Hello, world!"), Line::from("How are you?")])"#
)]
fn debug(#[case] text: Text, #[case] expected: &str) {
assert_eq!(format!("{text:?}"), expected);
}
#[test]
fn debug_alternate() {
let text = Text::from_iter([
Line::from("Hello, world!"),
Line::from("How are you?").bold().left_aligned(),
Line::from_iter([
Span::from("I'm "),
Span::from("doing ").italic(),
Span::from("great!").bold(),
]),
])
.on_blue()
.italic()
.centered();
assert_eq!(
format!("{text:#?}"),
indoc::indoc! {r#"
Text::from_iter([
Line::from("Hello, world!"),
Line::from("How are you?").bold().left_aligned(),
Line::from_iter([
Span::from("I'm "),
Span::from("doing ").italic(),
Span::from("great!").bold(),
]),
]).on_blue().italic().centered()"#}
);
}
}

View File

@@ -0,0 +1,8 @@
#![warn(missing_docs)]
//! The `widgets` module contains the `Widget` and `StatefulWidget` traits, which are used to
//! render UI elements on the screen.
pub use self::{stateful_widget::StatefulWidget, widget::Widget};
mod stateful_widget;
mod widget;

View File

@@ -0,0 +1,181 @@
use crate::{buffer::Buffer, layout::Rect};
/// A `StatefulWidget` is a widget that can take advantage of some local state to remember things
/// between two draw calls.
///
/// Most widgets can be drawn directly based on the input parameters. However, some features may
/// require some kind of associated state to be implemented.
///
/// For example, the `List` widget can highlight the item currently selected. This can be translated
/// in an offset, which is the number of elements to skip in order to have the selected item within
/// the viewport currently allocated to this widget. The widget can therefore only provide the
/// following behavior: whenever the selected item is out of the viewport scroll to a predefined
/// position (making the selected item the last viewable item or the one in the middle for example).
/// Nonetheless, if the widget has access to the last computed offset then it can implement a
/// natural scrolling experience where the last offset is reused until the selected item is out of
/// the viewport.
///
/// ## Examples
///
/// ```rust,ignore
/// use std::io;
///
/// use ratatui::{
/// backend::TestBackend,
/// widgets::{List, ListItem, ListState, StatefulWidget, Widget},
/// Terminal,
/// };
///
/// // Let's say we have some events to display.
/// struct Events {
/// // `items` is the state managed by your application.
/// items: Vec<String>,
/// // `state` is the state that can be modified by the UI. It stores the index of the selected
/// // item as well as the offset computed during the previous draw call (used to implement
/// // natural scrolling).
/// state: ListState,
/// }
///
/// impl Events {
/// fn new(items: Vec<String>) -> Events {
/// Events {
/// items,
/// state: ListState::default(),
/// }
/// }
///
/// pub fn set_items(&mut self, items: Vec<String>) {
/// self.items = items;
/// // We reset the state as the associated items have changed. This effectively reset
/// // the selection as well as the stored offset.
/// self.state = ListState::default();
/// }
///
/// // Select the next item. This will not be reflected until the widget is drawn in the
/// // `Terminal::draw` callback using `Frame::render_stateful_widget`.
/// 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));
/// }
///
/// // Select the previous item. This will not be reflected until the widget is drawn in the
/// // `Terminal::draw` callback using `Frame::render_stateful_widget`.
/// 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));
/// }
///
/// // Unselect the currently selected item if any. The implementation of `ListState` makes
/// // sure that the stored offset is also reset.
/// pub fn unselect(&mut self) {
/// self.state.select(None);
/// }
/// }
///
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
///
/// let mut events = Events::new(vec![String::from("Item 1"), String::from("Item 2")]);
///
/// loop {
/// terminal.draw(|f| {
/// // The items managed by the application are transformed to something
/// // that is understood by ratatui.
/// let items: Vec<ListItem> = events
/// .items
/// .iter()
/// .map(|i| ListItem::new(i.as_str()))
/// .collect();
/// // The `List` widget is then built with those items.
/// let list = List::new(items);
/// // Finally the widget is rendered using the associated state. `events.state` is
/// // effectively the only thing that we will "remember" from this draw call.
/// f.render_stateful_widget(list, f.size(), &mut events.state);
/// });
///
/// // In response to some input events or an external http request or whatever:
/// events.next();
/// }
/// ```
pub trait StatefulWidget {
/// State associated with the stateful widget.
///
/// If you don't need this then you probably want to implement [`Widget`] instead.
///
/// [`Widget`]: super::Widget
type State: ?Sized;
/// Draws the current state of the widget in the given buffer. That is the only method required
/// to implement a custom stateful widget.
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
}
#[cfg(test)]
mod tests {
use rstest::{fixture, rstest};
use super::*;
use crate::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
#[fixture]
fn buf() -> Buffer {
Buffer::empty(Rect::new(0, 0, 20, 1))
}
#[fixture]
fn state() -> String {
"world".to_string()
}
struct PersonalGreeting;
impl StatefulWidget for PersonalGreeting {
type State = String;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
Line::from(format!("Hello {state}")).render(area, buf);
}
}
#[rstest]
fn render(mut buf: Buffer, mut state: String) {
let widget = PersonalGreeting;
widget.render(buf.area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
}
struct Bytes;
/// A widget with an unsized state type.
impl StatefulWidget for Bytes {
type State = [u8];
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let slice = std::str::from_utf8(state).unwrap();
Line::from(format!("Bytes: {slice}")).render(area, buf);
}
}
#[rstest]
fn render_unsized_state_type(mut buf: Buffer) {
let widget = Bytes;
let state = b"hello";
widget.render(buf.area, &mut buf, &mut state.clone());
assert_eq!(buf, Buffer::with_lines(["Bytes: hello "]));
}
}

View File

@@ -0,0 +1,160 @@
use crate::{buffer::Buffer, layout::Rect, style::Style};
/// A `Widget` is a type that can be drawn on a [`Buffer`] in a given [`Rect`].
///
/// Prior to Ratatui 0.26.0, widgets generally were created for each frame as they were consumed
/// during rendering. This meant that they were not meant to be stored but used as *commands* to
/// draw common figures in the UI.
///
/// Starting with Ratatui 0.26.0, all the internal widgets implement Widget for a reference to
/// themselves. This allows you to store a reference to a widget and render it later. Widget crates
/// should consider also doing this to allow for more flexibility in how widgets are used.
///
/// In Ratatui 0.26.0, we also added an unstable `WidgetRef` trait and implemented this on all the
/// internal widgets. In addition to the above benefit of rendering references to widgets, this also
/// allows you to render boxed widgets. This is useful when you want to store a collection of
/// widgets with different types. You can then iterate over the collection and render each widget.
/// See <https://github.com/ratatui/ratatui/issues/1287> for more information.
///
/// In general where you expect a widget to immutably work on its data, we recommended to implement
/// `Widget` for a reference to the widget (`impl Widget for &MyWidget`). If you need to store state
/// between draw calls, implement `StatefulWidget` if you want the Widget to be immutable, or
/// implement `Widget` for a mutable reference to the widget (`impl Widget for &mut MyWidget`) if
/// you want the widget to be mutable. The mutable widget pattern is used infrequently in apps, but
/// can be quite useful.
///
/// A blanket implementation of `Widget` for `&W` where `W` implements `WidgetRef` is provided.
/// Widget is also implemented for `&str` and `String` types.
///
/// # Examples
///
/// ```rust,ignore
/// use ratatui::{
/// backend::TestBackend,
/// widgets::{Clear, Widget},
/// Terminal,
/// };
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
///
/// terminal.draw(|frame| {
/// frame.render_widget(Clear, frame.area());
/// });
/// ```
///
/// It's common to render widgets inside other widgets:
///
/// ```rust
/// use ratatui_core::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
///
/// struct MyWidget;
///
/// impl Widget for MyWidget {
/// fn render(self, area: Rect, buf: &mut Buffer) {
/// Line::raw("Hello").render(area, buf);
/// }
/// }
/// ```
pub trait Widget {
/// Draws the current state of the widget in the given buffer. That is the only method required
/// to implement a custom widget.
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized;
}
/// Renders a string slice as a widget.
///
/// This implementation allows a string slice (`&str`) to act as a widget, meaning it can be drawn
/// onto a [`Buffer`] in a specified [`Rect`]. The slice represents a static string which can be
/// rendered by reference, thereby avoiding the need for string cloning or ownership transfer when
/// drawing the text to the screen.
impl Widget for &str {
fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_stringn(area.x, area.y, self, area.width as usize, Style::new());
}
}
/// Renders a `String` object as a widget.
///
/// This implementation enables an owned `String` to be treated as a widget, which can be rendered
/// on a [`Buffer`] within the bounds of a given [`Rect`].
impl Widget for String {
fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_stringn(area.x, area.y, self, area.width as usize, Style::new());
}
}
impl<W: Widget> Widget for Option<W> {
fn render(self, area: Rect, buf: &mut Buffer) {
if let Some(widget) = self {
widget.render(area, buf);
}
}
}
#[cfg(test)]
mod tests {
use rstest::{fixture, rstest};
use super::*;
use crate::{buffer::Buffer, layout::Rect, text::Line};
#[fixture]
fn buf() -> Buffer {
Buffer::empty(Rect::new(0, 0, 20, 1))
}
struct Greeting;
impl Widget for Greeting {
fn render(self, area: Rect, buf: &mut Buffer) {
Line::from("Hello").render(area, buf);
}
}
#[rstest]
fn render(mut buf: Buffer) {
let widget = Greeting;
widget.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
#[rstest]
fn render_str(mut buf: Buffer) {
"hello world".render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_str_truncate(mut buf: Buffer) {
let area = Rect::new(buf.area.x, buf.area.y, 11, buf.area.height);
"hello world, just hello".render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_option_str(mut buf: Buffer) {
Some("hello world").render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_string(mut buf: Buffer) {
String::from("hello world").render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_string_truncate(mut buf: Buffer) {
let area = Rect::new(buf.area.x, buf.area.y, 11, buf.area.height);
String::from("hello world, just hello").render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_option_string(mut buf: Buffer) {
Some(String::from("hello world")).render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
}

View File

@@ -0,0 +1,45 @@
[package]
name = "ratatui-crossterm"
version = "0.1.0-alpha.0"
description = "Crossterm backend for the Ratatui Terminal UI library."
documentation = "https://docs.rs/ratatui-crossterm/"
readme = "README.md"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
keywords.workspace = true
categories.workspace = true
license.workspace = true
exclude.workspace = true
edition.workspace = true
rust-version.workspace = true
[features]
default = ["underline-color"]
## 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 = ["ratatui-core/underline-color"]
## Use terminal scrolling regions to make Terminal::insert_before less prone to flickering.
scrolling-regions = ["ratatui-core/scrolling-regions"]
#! The following features are unstable and may change in the future:
## Enable all unstable features.
unstable = ["unstable-backend-writer"]
## Enables getting access to backends' writers.
unstable-backend-writer = []
[dependencies]
crossterm.workspace = true
document-features = { workspace = true, optional = true }
instability.workspace = true
ratatui-core = { workspace = true }
[dev-dependencies]
ratatui = { path = "../ratatui", features = ["crossterm"] }
rstest.workspace = true

View File

@@ -0,0 +1,10 @@
# Ratatui-crossterm
<!-- cargo-rdme start -->
This module provides the [`CrosstermBackend`] implementation for the [`Backend`] trait. It uses
the [Crossterm] crate to interact with the terminal.
[Crossterm]: https://crates.io/crates/crossterm
<!-- cargo-rdme end -->

View File

@@ -0,0 +1,812 @@
// show the feature flags in the generated documentation
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png",
html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico"
)]
#![warn(missing_docs)]
//! This module provides the [`CrosstermBackend`] implementation for the [`Backend`] trait. It uses
//! the [Crossterm] crate to interact with the terminal.
//!
//! [Crossterm]: https://crates.io/crates/crossterm
#![cfg_attr(feature = "document-features", doc = "\n## Features")]
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
use std::io::{self, Write};
pub use crossterm;
#[cfg(feature = "underline-color")]
use crossterm::style::SetUnderlineColor;
use crossterm::{
cursor::{Hide, MoveTo, Show},
execute, queue,
style::{
Attribute as CrosstermAttribute, Attributes as CrosstermAttributes,
Color as CrosstermColor, Colors as CrosstermColors, ContentStyle, Print, SetAttribute,
SetBackgroundColor, SetColors, SetForegroundColor,
},
terminal::{self, Clear},
};
use ratatui_core::{
backend::{Backend, ClearType, WindowSize},
buffer::Cell,
layout::{Position, Size},
style::{Color, Modifier, Style},
};
/// A [`Backend`] implementation that uses [Crossterm] to render to the terminal.
///
/// The `CrosstermBackend` struct is a wrapper around a writer implementing [`Write`], which is
/// used to send commands to the terminal. It provides methods for drawing content, manipulating
/// the cursor, and clearing the terminal screen.
///
/// Most applications should not call the methods on `CrosstermBackend` directly, but will instead
/// use the [`Terminal`] struct, which provides a more ergonomic interface.
///
/// Usually applications will enable raw mode and switch to alternate screen mode after creating
/// a `CrosstermBackend`. This is done by calling [`crossterm::terminal::enable_raw_mode`] and
/// [`crossterm::terminal::EnterAlternateScreen`] (and the corresponding disable/leave functions
/// when the application exits). This is not done automatically by the backend because it is
/// possible that the application may want to use the terminal for other purposes (like showing
/// help text) before entering alternate screen mode.
///
/// # Example
///
/// ```rust,no_run
/// use std::io::{stderr, stdout};
///
/// use crossterm::{
/// terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
/// ExecutableCommand,
/// };
/// use ratatui::{backend::CrosstermBackend, Terminal};
///
/// let mut backend = CrosstermBackend::new(stdout());
/// // or
/// let backend = CrosstermBackend::new(stderr());
/// let mut terminal = Terminal::new(backend)?;
///
/// enable_raw_mode()?;
/// stdout().execute(EnterAlternateScreen)?;
///
/// terminal.clear()?;
/// terminal.draw(|frame| {
/// // -- snip --
/// })?;
///
/// stdout().execute(LeaveAlternateScreen)?;
/// disable_raw_mode()?;
///
/// # std::io::Result::Ok(())
/// ```
///
/// See the the [Examples] directory for more examples. See the [`backend`] module documentation
/// for more details on raw mode and alternate screen.
///
/// [`Write`]: std::io::Write
/// [`Terminal`]: https://docs.rs/ratatui/latest/ratatui/struct.Terminal.html
/// [`backend`]: ratatui_core::backend
/// [Crossterm]: https://crates.io/crates/crossterm
/// [Examples]: https://github.com/ratatui/ratatui/tree/main/ratatui/examples/README.md
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct CrosstermBackend<W: Write> {
/// The writer used to send commands to the terminal.
writer: W,
}
impl<W> CrosstermBackend<W>
where
W: Write,
{
/// Creates a new `CrosstermBackend` with the given writer.
///
/// Most applications will use either [`stdout`](std::io::stdout) or
/// [`stderr`](std::io::stderr) as writer. See the [FAQ] to determine which one to use.
///
/// [FAQ]: https://ratatui.rs/faq/#should-i-use-stdout-or-stderr
///
/// # Example
///
/// ```rust,no_run
/// use std::io::stdout;
///
/// use ratatui::backend::CrosstermBackend;
///
/// let backend = CrosstermBackend::new(stdout());
/// ```
pub const fn new(writer: W) -> Self {
Self { writer }
}
/// Gets the writer.
#[instability::unstable(
feature = "backend-writer",
issue = "https://github.com/ratatui/ratatui/pull/991"
)]
pub const fn writer(&self) -> &W {
&self.writer
}
/// Gets the writer as a mutable reference.
///
/// Note: writing to the writer may cause incorrect output after the write. This is due to the
/// way that the Terminal implements diffing Buffers.
#[instability::unstable(
feature = "backend-writer",
issue = "https://github.com/ratatui/ratatui/pull/991"
)]
pub fn writer_mut(&mut self) -> &mut W {
&mut self.writer
}
}
impl<W> Write for CrosstermBackend<W>
where
W: Write,
{
/// Writes a buffer of bytes to the underlying buffer.
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.writer.write(buf)
}
/// Flushes the underlying buffer.
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
}
}
impl<W> Backend for CrosstermBackend<W>
where
W: Write,
{
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
let mut fg = Color::Reset;
let mut bg = Color::Reset;
#[cfg(feature = "underline-color")]
let mut underline_color = Color::Reset;
let mut modifier = Modifier::empty();
let mut last_pos: Option<Position> = None;
for (x, y, cell) in content {
// Move the cursor if the previous location was not (x - 1, y)
if !matches!(last_pos, Some(p) if x == p.x + 1 && y == p.y) {
queue!(self.writer, MoveTo(x, y))?;
}
last_pos = Some(Position { x, y });
if cell.modifier != modifier {
let diff = ModifierDiff {
from: modifier,
to: cell.modifier,
};
diff.queue(&mut self.writer)?;
modifier = cell.modifier;
}
if cell.fg != fg || cell.bg != bg {
queue!(
self.writer,
SetColors(CrosstermColors::new(
cell.fg.into_crossterm(),
cell.bg.into_crossterm(),
))
)?;
fg = cell.fg;
bg = cell.bg;
}
#[cfg(feature = "underline-color")]
if cell.underline_color != underline_color {
let color = cell.underline_color.into_crossterm();
queue!(self.writer, SetUnderlineColor(color))?;
underline_color = cell.underline_color;
}
queue!(self.writer, Print(cell.symbol()))?;
}
#[cfg(feature = "underline-color")]
return queue!(
self.writer,
SetForegroundColor(CrosstermColor::Reset),
SetBackgroundColor(CrosstermColor::Reset),
SetUnderlineColor(CrosstermColor::Reset),
SetAttribute(CrosstermAttribute::Reset),
);
#[cfg(not(feature = "underline-color"))]
return queue!(
self.writer,
SetForegroundColor(CrosstermColor::Reset),
SetBackgroundColor(CrosstermColor::Reset),
SetAttribute(CrosstermAttribute::Reset),
);
}
fn hide_cursor(&mut self) -> io::Result<()> {
execute!(self.writer, Hide)
}
fn show_cursor(&mut self) -> io::Result<()> {
execute!(self.writer, Show)
}
fn get_cursor_position(&mut self) -> io::Result<Position> {
crossterm::cursor::position()
.map(|(x, y)| Position { x, y })
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
}
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
let Position { x, y } = position.into();
execute!(self.writer, MoveTo(x, y))
}
fn clear(&mut self) -> io::Result<()> {
self.clear_region(ClearType::All)
}
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
execute!(
self.writer,
Clear(match clear_type {
ClearType::All => crossterm::terminal::ClearType::All,
ClearType::AfterCursor => crossterm::terminal::ClearType::FromCursorDown,
ClearType::BeforeCursor => crossterm::terminal::ClearType::FromCursorUp,
ClearType::CurrentLine => crossterm::terminal::ClearType::CurrentLine,
ClearType::UntilNewLine => crossterm::terminal::ClearType::UntilNewLine,
})
)
}
fn append_lines(&mut self, n: u16) -> io::Result<()> {
for _ in 0..n {
queue!(self.writer, Print("\n"))?;
}
self.writer.flush()
}
fn size(&self) -> io::Result<Size> {
let (width, height) = terminal::size()?;
Ok(Size { width, height })
}
fn window_size(&mut self) -> io::Result<WindowSize> {
let crossterm::terminal::WindowSize {
columns,
rows,
width,
height,
} = terminal::window_size()?;
Ok(WindowSize {
columns_rows: Size {
width: columns,
height: rows,
},
pixels: Size { width, height },
})
}
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
}
#[cfg(feature = "scrolling-regions")]
fn scroll_region_up(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
queue!(
self.writer,
ScrollUpInRegion {
first_row: region.start,
last_row: region.end.saturating_sub(1),
lines_to_scroll: amount,
}
)?;
self.writer.flush()
}
#[cfg(feature = "scrolling-regions")]
fn scroll_region_down(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
queue!(
self.writer,
ScrollDownInRegion {
first_row: region.start,
last_row: region.end.saturating_sub(1),
lines_to_scroll: amount,
}
)?;
self.writer.flush()
}
}
/// A trait for converting a Ratatui type to a Crossterm type.
///
/// This trait is needed for avoiding the orphan rule when implementing `From` for crossterm types
/// once these are moved to a separate crate.
pub trait IntoCrossterm<C> {
/// Converts the ratatui type to a crossterm type.
fn into_crossterm(self) -> C;
}
/// A trait for converting a Crossterm type to a Ratatui type.
///
/// This trait is needed for avoiding the orphan rule when implementing `From` for crossterm types
/// once these are moved to a separate crate.
pub trait FromCrossterm<C> {
/// Converts the crossterm type to a ratatui type.
fn from_crossterm(value: C) -> Self;
}
impl IntoCrossterm<CrosstermColor> for Color {
fn into_crossterm(self) -> CrosstermColor {
match self {
Self::Reset => CrosstermColor::Reset,
Self::Black => CrosstermColor::Black,
Self::Red => CrosstermColor::DarkRed,
Self::Green => CrosstermColor::DarkGreen,
Self::Yellow => CrosstermColor::DarkYellow,
Self::Blue => CrosstermColor::DarkBlue,
Self::Magenta => CrosstermColor::DarkMagenta,
Self::Cyan => CrosstermColor::DarkCyan,
Self::Gray => CrosstermColor::Grey,
Self::DarkGray => CrosstermColor::DarkGrey,
Self::LightRed => CrosstermColor::Red,
Self::LightGreen => CrosstermColor::Green,
Self::LightBlue => CrosstermColor::Blue,
Self::LightYellow => CrosstermColor::Yellow,
Self::LightMagenta => CrosstermColor::Magenta,
Self::LightCyan => CrosstermColor::Cyan,
Self::White => CrosstermColor::White,
Self::Indexed(i) => CrosstermColor::AnsiValue(i),
Self::Rgb(r, g, b) => CrosstermColor::Rgb { r, g, b },
}
}
}
impl FromCrossterm<CrosstermColor> for Color {
fn from_crossterm(value: CrosstermColor) -> Self {
match value {
CrosstermColor::Reset => Self::Reset,
CrosstermColor::Black => Self::Black,
CrosstermColor::DarkRed => Self::Red,
CrosstermColor::DarkGreen => Self::Green,
CrosstermColor::DarkYellow => Self::Yellow,
CrosstermColor::DarkBlue => Self::Blue,
CrosstermColor::DarkMagenta => Self::Magenta,
CrosstermColor::DarkCyan => Self::Cyan,
CrosstermColor::Grey => Self::Gray,
CrosstermColor::DarkGrey => Self::DarkGray,
CrosstermColor::Red => Self::LightRed,
CrosstermColor::Green => Self::LightGreen,
CrosstermColor::Blue => Self::LightBlue,
CrosstermColor::Yellow => Self::LightYellow,
CrosstermColor::Magenta => Self::LightMagenta,
CrosstermColor::Cyan => Self::LightCyan,
CrosstermColor::White => Self::White,
CrosstermColor::Rgb { r, g, b } => Self::Rgb(r, g, b),
CrosstermColor::AnsiValue(v) => Self::Indexed(v),
}
}
}
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
/// values. This is useful when updating the terminal display, as it allows for more
/// efficient updates by only sending the necessary changes.
struct ModifierDiff {
pub from: Modifier,
pub to: Modifier,
}
impl ModifierDiff {
fn queue<W>(self, mut w: W) -> io::Result<()>
where
W: io::Write,
{
//use crossterm::Attribute;
let removed = self.from - self.to;
if removed.contains(Modifier::REVERSED) {
queue!(w, SetAttribute(CrosstermAttribute::NoReverse))?;
}
if removed.contains(Modifier::BOLD) {
queue!(w, SetAttribute(CrosstermAttribute::NormalIntensity))?;
if self.to.contains(Modifier::DIM) {
queue!(w, SetAttribute(CrosstermAttribute::Dim))?;
}
}
if removed.contains(Modifier::ITALIC) {
queue!(w, SetAttribute(CrosstermAttribute::NoItalic))?;
}
if removed.contains(Modifier::UNDERLINED) {
queue!(w, SetAttribute(CrosstermAttribute::NoUnderline))?;
}
if removed.contains(Modifier::DIM) {
queue!(w, SetAttribute(CrosstermAttribute::NormalIntensity))?;
}
if removed.contains(Modifier::CROSSED_OUT) {
queue!(w, SetAttribute(CrosstermAttribute::NotCrossedOut))?;
}
if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
queue!(w, SetAttribute(CrosstermAttribute::NoBlink))?;
}
let added = self.to - self.from;
if added.contains(Modifier::REVERSED) {
queue!(w, SetAttribute(CrosstermAttribute::Reverse))?;
}
if added.contains(Modifier::BOLD) {
queue!(w, SetAttribute(CrosstermAttribute::Bold))?;
}
if added.contains(Modifier::ITALIC) {
queue!(w, SetAttribute(CrosstermAttribute::Italic))?;
}
if added.contains(Modifier::UNDERLINED) {
queue!(w, SetAttribute(CrosstermAttribute::Underlined))?;
}
if added.contains(Modifier::DIM) {
queue!(w, SetAttribute(CrosstermAttribute::Dim))?;
}
if added.contains(Modifier::CROSSED_OUT) {
queue!(w, SetAttribute(CrosstermAttribute::CrossedOut))?;
}
if added.contains(Modifier::SLOW_BLINK) {
queue!(w, SetAttribute(CrosstermAttribute::SlowBlink))?;
}
if added.contains(Modifier::RAPID_BLINK) {
queue!(w, SetAttribute(CrosstermAttribute::RapidBlink))?;
}
Ok(())
}
}
impl FromCrossterm<CrosstermAttribute> for Modifier {
fn from_crossterm(value: CrosstermAttribute) -> Self {
// `Attribute*s*` (note the *s*) contains multiple `Attribute` We convert `Attribute` to
// `Attribute*s*` (containing only 1 value) to avoid implementing the conversion again
Self::from_crossterm(CrosstermAttributes::from(value))
}
}
impl FromCrossterm<CrosstermAttributes> for Modifier {
fn from_crossterm(value: CrosstermAttributes) -> Self {
let mut res = Self::empty();
if value.has(CrosstermAttribute::Bold) {
res |= Self::BOLD;
}
if value.has(CrosstermAttribute::Dim) {
res |= Self::DIM;
}
if value.has(CrosstermAttribute::Italic) {
res |= Self::ITALIC;
}
if value.has(CrosstermAttribute::Underlined)
|| value.has(CrosstermAttribute::DoubleUnderlined)
|| value.has(CrosstermAttribute::Undercurled)
|| value.has(CrosstermAttribute::Underdotted)
|| value.has(CrosstermAttribute::Underdashed)
{
res |= Self::UNDERLINED;
}
if value.has(CrosstermAttribute::SlowBlink) {
res |= Self::SLOW_BLINK;
}
if value.has(CrosstermAttribute::RapidBlink) {
res |= Self::RAPID_BLINK;
}
if value.has(CrosstermAttribute::Reverse) {
res |= Self::REVERSED;
}
if value.has(CrosstermAttribute::Hidden) {
res |= Self::HIDDEN;
}
if value.has(CrosstermAttribute::CrossedOut) {
res |= Self::CROSSED_OUT;
}
res
}
}
impl FromCrossterm<ContentStyle> for Style {
fn from_crossterm(value: ContentStyle) -> Self {
let mut sub_modifier = Modifier::empty();
if value.attributes.has(CrosstermAttribute::NoBold) {
sub_modifier |= Modifier::BOLD;
}
if value.attributes.has(CrosstermAttribute::NoItalic) {
sub_modifier |= Modifier::ITALIC;
}
if value.attributes.has(CrosstermAttribute::NotCrossedOut) {
sub_modifier |= Modifier::CROSSED_OUT;
}
if value.attributes.has(CrosstermAttribute::NoUnderline) {
sub_modifier |= Modifier::UNDERLINED;
}
if value.attributes.has(CrosstermAttribute::NoHidden) {
sub_modifier |= Modifier::HIDDEN;
}
if value.attributes.has(CrosstermAttribute::NoBlink) {
sub_modifier |= Modifier::RAPID_BLINK | Modifier::SLOW_BLINK;
}
if value.attributes.has(CrosstermAttribute::NoReverse) {
sub_modifier |= Modifier::REVERSED;
}
Self {
fg: value.foreground_color.map(FromCrossterm::from_crossterm),
bg: value.background_color.map(FromCrossterm::from_crossterm),
#[cfg(feature = "underline-color")]
underline_color: value.underline_color.map(FromCrossterm::from_crossterm),
add_modifier: Modifier::from_crossterm(value.attributes),
sub_modifier,
}
}
}
/// A command that scrolls the terminal screen a given number of rows up in a specific scrolling
/// region.
///
/// This will hopefully be replaced by a struct in crossterm proper. There are two outstanding
/// crossterm PRs that will address this:
/// - [918](https://github.com/crossterm-rs/crossterm/pull/918)
/// - [923](https://github.com/crossterm-rs/crossterm/pull/923)
#[cfg(feature = "scrolling-regions")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ScrollUpInRegion {
/// The first row of the scrolling region.
pub first_row: u16,
/// The last row of the scrolling region.
pub last_row: u16,
/// The number of lines to scroll up by.
pub lines_to_scroll: u16,
}
#[cfg(feature = "scrolling-regions")]
impl crate::crossterm::Command for ScrollUpInRegion {
fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
if self.lines_to_scroll != 0 {
// Set a scrolling region that contains just the desired lines.
write!(
f,
crate::crossterm::csi!("{};{}r"),
self.first_row.saturating_add(1),
self.last_row.saturating_add(1)
)?;
// Scroll the region by the desired count.
write!(f, crate::crossterm::csi!("{}S"), self.lines_to_scroll)?;
// Reset the scrolling region to be the whole screen.
write!(f, crate::crossterm::csi!("r"))?;
}
Ok(())
}
#[cfg(windows)]
fn execute_winapi(&self) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"ScrollUpInRegion command not supported for winapi",
))
}
}
/// A command that scrolls the terminal screen a given number of rows down in a specific scrolling
/// region.
///
/// This will hopefully be replaced by a struct in crossterm proper. There are two outstanding
/// crossterm PRs that will address this:
/// - [918](https://github.com/crossterm-rs/crossterm/pull/918)
/// - [923](https://github.com/crossterm-rs/crossterm/pull/923)
#[cfg(feature = "scrolling-regions")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ScrollDownInRegion {
/// The first row of the scrolling region.
pub first_row: u16,
/// The last row of the scrolling region.
pub last_row: u16,
/// The number of lines to scroll down by.
pub lines_to_scroll: u16,
}
#[cfg(feature = "scrolling-regions")]
impl crate::crossterm::Command for ScrollDownInRegion {
fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
if self.lines_to_scroll != 0 {
// Set a scrolling region that contains just the desired lines.
write!(
f,
crate::crossterm::csi!("{};{}r"),
self.first_row.saturating_add(1),
self.last_row.saturating_add(1)
)?;
// Scroll the region by the desired count.
write!(f, crate::crossterm::csi!("{}T"), self.lines_to_scroll)?;
// Reset the scrolling region to be the whole screen.
write!(f, crate::crossterm::csi!("r"))?;
}
Ok(())
}
#[cfg(windows)]
fn execute_winapi(&self) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"ScrollDownInRegion command not supported for winapi",
))
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case(CrosstermColor::Reset, Color::Reset)]
#[case(CrosstermColor::Black, Color::Black)]
#[case(CrosstermColor::DarkGrey, Color::DarkGray)]
#[case(CrosstermColor::Red, Color::LightRed)]
#[case(CrosstermColor::DarkRed, Color::Red)]
#[case(CrosstermColor::Green, Color::LightGreen)]
#[case(CrosstermColor::DarkGreen, Color::Green)]
#[case(CrosstermColor::Yellow, Color::LightYellow)]
#[case(CrosstermColor::DarkYellow, Color::Yellow)]
#[case(CrosstermColor::Blue, Color::LightBlue)]
#[case(CrosstermColor::DarkBlue, Color::Blue)]
#[case(CrosstermColor::Magenta, Color::LightMagenta)]
#[case(CrosstermColor::DarkMagenta, Color::Magenta)]
#[case(CrosstermColor::Cyan, Color::LightCyan)]
#[case(CrosstermColor::DarkCyan, Color::Cyan)]
#[case(CrosstermColor::White, Color::White)]
#[case(CrosstermColor::Grey, Color::Gray)]
#[case(CrosstermColor::Rgb { r: 0, g: 0, b: 0 }, Color::Rgb(0, 0, 0) )]
#[case(CrosstermColor::Rgb { r: 10, g: 20, b: 30 }, Color::Rgb(10, 20, 30) )]
#[case(CrosstermColor::AnsiValue(32), Color::Indexed(32))]
#[case(CrosstermColor::AnsiValue(37), Color::Indexed(37))]
fn from_crossterm_color(#[case] crossterm_color: CrosstermColor, #[case] color: Color) {
assert_eq!(Color::from_crossterm(crossterm_color), color);
}
mod modifier {
use super::*;
#[rstest]
#[case(CrosstermAttribute::Reset, Modifier::empty())]
#[case(CrosstermAttribute::Bold, Modifier::BOLD)]
#[case(CrosstermAttribute::NoBold, Modifier::empty())]
#[case(CrosstermAttribute::Italic, Modifier::ITALIC)]
#[case(CrosstermAttribute::NoItalic, Modifier::empty())]
#[case(CrosstermAttribute::Underlined, Modifier::UNDERLINED)]
#[case(CrosstermAttribute::NoUnderline, Modifier::empty())]
#[case(CrosstermAttribute::OverLined, Modifier::empty())]
#[case(CrosstermAttribute::NotOverLined, Modifier::empty())]
#[case(CrosstermAttribute::DoubleUnderlined, Modifier::UNDERLINED)]
#[case(CrosstermAttribute::Undercurled, Modifier::UNDERLINED)]
#[case(CrosstermAttribute::Underdotted, Modifier::UNDERLINED)]
#[case(CrosstermAttribute::Underdashed, Modifier::UNDERLINED)]
#[case(CrosstermAttribute::Dim, Modifier::DIM)]
#[case(CrosstermAttribute::NormalIntensity, Modifier::empty())]
#[case(CrosstermAttribute::CrossedOut, Modifier::CROSSED_OUT)]
#[case(CrosstermAttribute::NotCrossedOut, Modifier::empty())]
#[case(CrosstermAttribute::NoUnderline, Modifier::empty())]
#[case(CrosstermAttribute::SlowBlink, Modifier::SLOW_BLINK)]
#[case(CrosstermAttribute::RapidBlink, Modifier::RAPID_BLINK)]
#[case(CrosstermAttribute::Hidden, Modifier::HIDDEN)]
#[case(CrosstermAttribute::NoHidden, Modifier::empty())]
#[case(CrosstermAttribute::Reverse, Modifier::REVERSED)]
#[case(CrosstermAttribute::NoReverse, Modifier::empty())]
fn from_crossterm_attribute(
#[case] crossterm_attribute: CrosstermAttribute,
#[case] ratatui_modifier: Modifier,
) {
assert_eq!(
Modifier::from_crossterm(crossterm_attribute),
ratatui_modifier
);
}
#[rstest]
#[case(&[CrosstermAttribute::Bold], Modifier::BOLD)]
#[case(&[CrosstermAttribute::Bold, CrosstermAttribute::Italic], Modifier::BOLD | Modifier::ITALIC)]
#[case(&[CrosstermAttribute::Bold, CrosstermAttribute::NotCrossedOut], Modifier::BOLD)]
#[case(&[CrosstermAttribute::Dim, CrosstermAttribute::Underdotted], Modifier::DIM | Modifier::UNDERLINED)]
#[case(&[CrosstermAttribute::Dim, CrosstermAttribute::SlowBlink, CrosstermAttribute::Italic], Modifier::DIM | Modifier::SLOW_BLINK | Modifier::ITALIC)]
#[case(&[CrosstermAttribute::Hidden, CrosstermAttribute::NoUnderline, CrosstermAttribute::NotCrossedOut], Modifier::HIDDEN)]
#[case(&[CrosstermAttribute::Reverse], Modifier::REVERSED)]
#[case(&[CrosstermAttribute::Reset], Modifier::empty())]
#[case(&[CrosstermAttribute::RapidBlink, CrosstermAttribute::CrossedOut], Modifier::RAPID_BLINK | Modifier::CROSSED_OUT)]
fn from_crossterm_attributes(
#[case] crossterm_attributes: &[CrosstermAttribute],
#[case] ratatui_modifier: Modifier,
) {
assert_eq!(
Modifier::from_crossterm(CrosstermAttributes::from(crossterm_attributes)),
ratatui_modifier
);
}
}
#[rstest]
#[case(ContentStyle::default(), Style::default())]
#[case(
ContentStyle {
foreground_color: Some(CrosstermColor::DarkYellow),
..Default::default()
},
Style::default().fg(Color::Yellow)
)]
#[case(
ContentStyle {
background_color: Some(CrosstermColor::DarkYellow),
..Default::default()
},
Style::default().bg(Color::Yellow)
)]
#[case(
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::Bold),
..Default::default()
},
Style::default().add_modifier(Modifier::BOLD)
)]
#[case(
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::NoBold),
..Default::default()
},
Style::default().remove_modifier(Modifier::BOLD)
)]
#[case(
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::Italic),
..Default::default()
},
Style::default().add_modifier(Modifier::ITALIC)
)]
#[case(
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::NoItalic),
..Default::default()
},
Style::default().remove_modifier(Modifier::ITALIC)
)]
#[case(
ContentStyle {
attributes: CrosstermAttributes::from(
[CrosstermAttribute::Bold, CrosstermAttribute::Italic].as_ref()
),
..Default::default()
},
Style::default()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::ITALIC)
)]
#[case(
ContentStyle {
attributes: CrosstermAttributes::from(
[CrosstermAttribute::NoBold, CrosstermAttribute::NoItalic].as_ref()
),
..Default::default()
},
Style::default()
.remove_modifier(Modifier::BOLD)
.remove_modifier(Modifier::ITALIC)
)]
fn from_crossterm_content_style(#[case] content_style: ContentStyle, #[case] style: Style) {
assert_eq!(Style::from_crossterm(content_style), style);
}
#[test]
#[cfg(feature = "underline-color")]
fn from_crossterm_content_style_underline() {
let content_style = ContentStyle {
underline_color: Some(CrosstermColor::DarkRed),
..Default::default()
};
assert_eq!(
Style::from_crossterm(content_style),
Style::default().underline_color(Color::Red)
);
}
}

View File

@@ -0,0 +1,35 @@
[package]
name = "ratatui-termion"
version = "0.1.0-alpha.0"
description = "Termion backend for the Ratatui Terminal UI library."
documentation = "https://docs.rs/ratatui-termion/"
readme = "README.md"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
keywords.workspace = true
categories.workspace = true
license.workspace = true
exclude.workspace = true
edition.workspace = true
rust-version.workspace = true
[package.metadata.docs.rs]
all-features = true
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
rustdoc-args = ["--cfg", "docsrs"]
[features]
default = []
## Use terminal scrolling regions to make Terminal::insert_before less prone to flickering.
scrolling-regions = ["ratatui-core/scrolling-regions"]
[dependencies]
document-features = { workspace = true, optional = true }
ratatui-core = { workspace = true }
termion.workspace = true
instability.workspace = true
[dev-dependencies]
rstest.workspace = true

11
ratatui-termion/README.md Normal file
View File

@@ -0,0 +1,11 @@
# Ratatui-termion
<!-- cargo-rdme start -->
This module provides the [`TermionBackend`] implementation for the [`Backend`] trait. It uses
the [Termion] crate to interact with the terminal.
[`Backend`]: ratatui_core::backend::Backend
[Termion]: https://docs.rs/termion
<!-- cargo-rdme end -->

View File

@@ -1,21 +1,32 @@
// show the feature flags in the generated documentation
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png",
html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico"
)]
#![warn(missing_docs)]
//! This module provides the [`TermionBackend`] implementation for the [`Backend`] trait. It uses
//! the [Termion] crate to interact with the terminal.
//!
//! [`Backend`]: crate::backend::Backend
//! [`TermionBackend`]: crate::backend::TermionBackend
//! [`Backend`]: ratatui_core::backend::Backend
//! [Termion]: https://docs.rs/termion
#![cfg_attr(feature = "document-features", doc = "\n## Features")]
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
use std::{
fmt,
io::{self, Write},
};
use crate::{
use ratatui_core::{
backend::{Backend, ClearType, WindowSize},
buffer::Cell,
layout::{Position, Size},
style::{Color, Modifier, Style},
termion::{self, color as tcolor, color::Color as _, style as tstyle},
};
pub use termion;
use termion::{color as tcolor, color::Color as _, style as tstyle};
/// A [`Backend`] implementation that uses [Termion] to render to the terminal.
///
@@ -40,8 +51,9 @@ use crate::{
/// use std::io::{stderr, stdout};
///
/// use ratatui::{
/// prelude::*,
/// backend::TermionBackend,
/// termion::{raw::IntoRawMode, screen::IntoAlternateScreen},
/// Terminal,
/// };
///
/// let writer = stdout().into_raw_mode()?.into_alternate_screen()?;
@@ -60,7 +72,7 @@ use crate::{
///
/// [`IntoRawMode::into_raw_mode()`]: termion::raw::IntoRawMode
/// [`IntoAlternateScreen::into_alternate_screen()`]: termion::screen::IntoAlternateScreen
/// [`Terminal`]: crate::terminal::Terminal
/// [`Terminal`]: ratatui::terminal::Terminal
/// [Termion]: https://docs.rs/termion
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct TermionBackend<W>
@@ -76,11 +88,18 @@ where
{
/// Creates a new Termion backend with the given writer.
///
/// Most applications will use either [`stdout`](std::io::stdout) or
/// [`stderr`](std::io::stderr) as writer. See the [FAQ] to determine which one to use.
///
/// [FAQ]: https://ratatui.rs/faq/#should-i-use-stdout-or-stderr
///
/// # Example
///
/// ```rust,no_run
/// # use std::io::stdout;
/// # use ratatui::prelude::*;
/// use std::io::stdout;
///
/// use ratatui::backend::TermionBackend;
///
/// let backend = TermionBackend::new(stdout());
/// ```
pub const fn new(writer: W) -> Self {
@@ -90,7 +109,7 @@ where
/// Gets the writer.
#[instability::unstable(
feature = "backend-writer",
issue = "https://github.com/ratatui-org/ratatui/pull/991"
issue = "https://github.com/ratatui/ratatui/pull/991"
)]
pub const fn writer(&self) -> &W {
&self.writer
@@ -101,7 +120,7 @@ where
/// way that the Terminal implements diffing Buffers.
#[instability::unstable(
feature = "backend-writer",
issue = "https://github.com/ratatui-org/ratatui/pull/991"
issue = "https://github.com/ratatui/ratatui/pull/991"
)]
pub fn writer_mut(&mut self) -> &mut W {
&mut self.writer
@@ -231,6 +250,30 @@ where
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
}
#[cfg(feature = "scrolling-regions")]
fn scroll_region_up(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
write!(
self.writer,
"{}{}{}",
SetRegion(region.start.saturating_add(1), region.end),
termion::scroll::Up(amount),
ResetRegion,
)?;
self.writer.flush()
}
#[cfg(feature = "scrolling-regions")]
fn scroll_region_down(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
write!(
self.writer,
"{}{}{}",
SetRegion(region.start.saturating_add(1), region.end),
termion::scroll::Down(amount),
ResetRegion,
)?;
self.writer.flush()
}
}
struct Fg(Color);
@@ -295,22 +338,40 @@ impl fmt::Display for Bg {
}
}
/// A trait for converting a Termion type to a Ratatui type.
///
/// This trait is necessary to avoid the orphan rule, as we cannot implement a trait for a type
/// defined in another crate.
pub trait FromTermion<T> {
/// Convert the Termion type to the Ratatui type.
fn from_termion(termion: T) -> Self;
}
/// A trait for converting a Ratatui type to a Termion type.
///
/// This trait is necessary to avoid the orphan rule, as we cannot implement a trait for a type
/// defined in another crate.
pub trait IntoTermion<T> {
/// Convert the Ratatui type to the Termion type.
fn into_termion(self) -> T;
}
macro_rules! from_termion_for_color {
($termion_color:ident, $color:ident) => {
impl From<tcolor::$termion_color> for Color {
fn from(_: tcolor::$termion_color) -> Self {
impl FromTermion<tcolor::$termion_color> for Color {
fn from_termion(_: tcolor::$termion_color) -> Self {
Color::$color
}
}
impl From<tcolor::Bg<tcolor::$termion_color>> for Style {
fn from(_: tcolor::Bg<tcolor::$termion_color>) -> Self {
impl FromTermion<tcolor::Bg<tcolor::$termion_color>> for Style {
fn from_termion(_: tcolor::Bg<tcolor::$termion_color>) -> Self {
Style::default().bg(Color::$color)
}
}
impl From<tcolor::Fg<tcolor::$termion_color>> for Style {
fn from(_: tcolor::Fg<tcolor::$termion_color>) -> Self {
impl FromTermion<tcolor::Fg<tcolor::$termion_color>> for Style {
fn from_termion(_: tcolor::Fg<tcolor::$termion_color>) -> Self {
Style::default().fg(Color::$color)
}
}
@@ -335,38 +396,38 @@ from_termion_for_color!(LightMagenta, LightMagenta);
from_termion_for_color!(LightCyan, LightCyan);
from_termion_for_color!(LightWhite, White);
impl From<tcolor::AnsiValue> for Color {
fn from(value: tcolor::AnsiValue) -> Self {
impl FromTermion<tcolor::AnsiValue> for Color {
fn from_termion(value: tcolor::AnsiValue) -> Self {
Self::Indexed(value.0)
}
}
impl From<tcolor::Bg<tcolor::AnsiValue>> for Style {
fn from(value: tcolor::Bg<tcolor::AnsiValue>) -> Self {
impl FromTermion<tcolor::Bg<tcolor::AnsiValue>> for Style {
fn from_termion(value: tcolor::Bg<tcolor::AnsiValue>) -> Self {
Self::default().bg(Color::Indexed(value.0 .0))
}
}
impl From<tcolor::Fg<tcolor::AnsiValue>> for Style {
fn from(value: tcolor::Fg<tcolor::AnsiValue>) -> Self {
impl FromTermion<tcolor::Fg<tcolor::AnsiValue>> for Style {
fn from_termion(value: tcolor::Fg<tcolor::AnsiValue>) -> Self {
Self::default().fg(Color::Indexed(value.0 .0))
}
}
impl From<tcolor::Rgb> for Color {
fn from(value: tcolor::Rgb) -> Self {
impl FromTermion<tcolor::Rgb> for Color {
fn from_termion(value: tcolor::Rgb) -> Self {
Self::Rgb(value.0, value.1, value.2)
}
}
impl From<tcolor::Bg<tcolor::Rgb>> for Style {
fn from(value: tcolor::Bg<tcolor::Rgb>) -> Self {
impl FromTermion<tcolor::Bg<tcolor::Rgb>> for Style {
fn from_termion(value: tcolor::Bg<tcolor::Rgb>) -> Self {
Self::default().bg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
}
}
impl From<tcolor::Fg<tcolor::Rgb>> for Style {
fn from(value: tcolor::Fg<tcolor::Rgb>) -> Self {
impl FromTermion<tcolor::Fg<tcolor::Rgb>> for Style {
fn from_termion(value: tcolor::Fg<tcolor::Rgb>) -> Self {
Self::default().fg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
}
}
@@ -438,8 +499,8 @@ impl fmt::Display for ModifierDiff {
macro_rules! from_termion_for_modifier {
($termion_modifier:ident, $modifier:ident) => {
impl From<tstyle::$termion_modifier> for Modifier {
fn from(_: tstyle::$termion_modifier) -> Self {
impl FromTermion<tstyle::$termion_modifier> for Modifier {
fn from_termion(_: tstyle::$termion_modifier) -> Self {
Modifier::$modifier
}
}
@@ -454,38 +515,68 @@ from_termion_for_modifier!(Faint, DIM);
from_termion_for_modifier!(CrossedOut, CROSSED_OUT);
from_termion_for_modifier!(Blink, SLOW_BLINK);
impl From<termion::style::Reset> for Modifier {
fn from(_: termion::style::Reset) -> Self {
impl FromTermion<termion::style::Reset> for Modifier {
fn from_termion(_: termion::style::Reset) -> Self {
Self::empty()
}
}
/// Set scrolling region.
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct SetRegion(pub u16, pub u16);
impl fmt::Display for SetRegion {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "\x1B[{};{}r", self.0, self.1)
}
}
/// Reset scrolling region.
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct ResetRegion;
impl fmt::Display for ResetRegion {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "\x1B[r")
}
}
#[cfg(test)]
mod tests {
use ratatui_core::style::Stylize;
use super::*;
use crate::style::Stylize;
#[test]
fn from_termion_color() {
assert_eq!(Color::from(tcolor::Reset), Color::Reset);
assert_eq!(Color::from(tcolor::Black), Color::Black);
assert_eq!(Color::from(tcolor::Red), Color::Red);
assert_eq!(Color::from(tcolor::Green), Color::Green);
assert_eq!(Color::from(tcolor::Yellow), Color::Yellow);
assert_eq!(Color::from(tcolor::Blue), Color::Blue);
assert_eq!(Color::from(tcolor::Magenta), Color::Magenta);
assert_eq!(Color::from(tcolor::Cyan), Color::Cyan);
assert_eq!(Color::from(tcolor::White), Color::Gray);
assert_eq!(Color::from(tcolor::LightBlack), Color::DarkGray);
assert_eq!(Color::from(tcolor::LightRed), Color::LightRed);
assert_eq!(Color::from(tcolor::LightGreen), Color::LightGreen);
assert_eq!(Color::from(tcolor::LightBlue), Color::LightBlue);
assert_eq!(Color::from(tcolor::LightYellow), Color::LightYellow);
assert_eq!(Color::from(tcolor::LightMagenta), Color::LightMagenta);
assert_eq!(Color::from(tcolor::LightCyan), Color::LightCyan);
assert_eq!(Color::from(tcolor::LightWhite), Color::White);
assert_eq!(Color::from(tcolor::AnsiValue(31)), Color::Indexed(31));
assert_eq!(Color::from(tcolor::Rgb(1, 2, 3)), Color::Rgb(1, 2, 3));
assert_eq!(Color::from_termion(tcolor::Reset), Color::Reset);
assert_eq!(Color::from_termion(tcolor::Black), Color::Black);
assert_eq!(Color::from_termion(tcolor::Red), Color::Red);
assert_eq!(Color::from_termion(tcolor::Green), Color::Green);
assert_eq!(Color::from_termion(tcolor::Yellow), Color::Yellow);
assert_eq!(Color::from_termion(tcolor::Blue), Color::Blue);
assert_eq!(Color::from_termion(tcolor::Magenta), Color::Magenta);
assert_eq!(Color::from_termion(tcolor::Cyan), Color::Cyan);
assert_eq!(Color::from_termion(tcolor::White), Color::Gray);
assert_eq!(Color::from_termion(tcolor::LightBlack), Color::DarkGray);
assert_eq!(Color::from_termion(tcolor::LightRed), Color::LightRed);
assert_eq!(Color::from_termion(tcolor::LightGreen), Color::LightGreen);
assert_eq!(Color::from_termion(tcolor::LightBlue), Color::LightBlue);
assert_eq!(Color::from_termion(tcolor::LightYellow), Color::LightYellow);
assert_eq!(
Color::from_termion(tcolor::LightMagenta),
Color::LightMagenta
);
assert_eq!(Color::from_termion(tcolor::LightCyan), Color::LightCyan);
assert_eq!(Color::from_termion(tcolor::LightWhite), Color::White);
assert_eq!(
Color::from_termion(tcolor::AnsiValue(31)),
Color::Indexed(31)
);
assert_eq!(
Color::from_termion(tcolor::Rgb(1, 2, 3)),
Color::Rgb(1, 2, 3)
);
}
#[test]
@@ -493,38 +584,62 @@ mod tests {
use tc::Bg;
use tcolor as tc;
assert_eq!(Style::from(Bg(tc::Reset)), Style::new().bg(Color::Reset));
assert_eq!(Style::from(Bg(tc::Black)), Style::new().on_black());
assert_eq!(Style::from(Bg(tc::Red)), Style::new().on_red());
assert_eq!(Style::from(Bg(tc::Green)), Style::new().on_green());
assert_eq!(Style::from(Bg(tc::Yellow)), Style::new().on_yellow());
assert_eq!(Style::from(Bg(tc::Blue)), Style::new().on_blue());
assert_eq!(Style::from(Bg(tc::Magenta)), Style::new().on_magenta());
assert_eq!(Style::from(Bg(tc::Cyan)), Style::new().on_cyan());
assert_eq!(Style::from(Bg(tc::White)), Style::new().on_gray());
assert_eq!(Style::from(Bg(tc::LightBlack)), Style::new().on_dark_gray());
assert_eq!(Style::from(Bg(tc::LightRed)), Style::new().on_light_red());
assert_eq!(
Style::from(Bg(tc::LightGreen)),
Style::from_termion(Bg(tc::Reset)),
Style::new().bg(Color::Reset)
);
assert_eq!(Style::from_termion(Bg(tc::Black)), Style::new().on_black());
assert_eq!(Style::from_termion(Bg(tc::Red)), Style::new().on_red());
assert_eq!(Style::from_termion(Bg(tc::Green)), Style::new().on_green());
assert_eq!(
Style::from_termion(Bg(tc::Yellow)),
Style::new().on_yellow()
);
assert_eq!(Style::from_termion(Bg(tc::Blue)), Style::new().on_blue());
assert_eq!(
Style::from_termion(Bg(tc::Magenta)),
Style::new().on_magenta()
);
assert_eq!(Style::from_termion(Bg(tc::Cyan)), Style::new().on_cyan());
assert_eq!(Style::from_termion(Bg(tc::White)), Style::new().on_gray());
assert_eq!(
Style::from_termion(Bg(tc::LightBlack)),
Style::new().on_dark_gray()
);
assert_eq!(
Style::from_termion(Bg(tc::LightRed)),
Style::new().on_light_red()
);
assert_eq!(
Style::from_termion(Bg(tc::LightGreen)),
Style::new().on_light_green()
);
assert_eq!(Style::from(Bg(tc::LightBlue)), Style::new().on_light_blue());
assert_eq!(
Style::from(Bg(tc::LightYellow)),
Style::from_termion(Bg(tc::LightBlue)),
Style::new().on_light_blue()
);
assert_eq!(
Style::from_termion(Bg(tc::LightYellow)),
Style::new().on_light_yellow()
);
assert_eq!(
Style::from(Bg(tc::LightMagenta)),
Style::from_termion(Bg(tc::LightMagenta)),
Style::new().on_light_magenta()
);
assert_eq!(Style::from(Bg(tc::LightCyan)), Style::new().on_light_cyan());
assert_eq!(Style::from(Bg(tc::LightWhite)), Style::new().on_white());
assert_eq!(
Style::from(Bg(tc::AnsiValue(31))),
Style::from_termion(Bg(tc::LightCyan)),
Style::new().on_light_cyan()
);
assert_eq!(
Style::from_termion(Bg(tc::LightWhite)),
Style::new().on_white()
);
assert_eq!(
Style::from_termion(Bg(tc::AnsiValue(31))),
Style::new().bg(Color::Indexed(31))
);
assert_eq!(
Style::from(Bg(tc::Rgb(1, 2, 3))),
Style::from_termion(Bg(tc::Rgb(1, 2, 3))),
Style::new().bg(Color::Rgb(1, 2, 3))
);
}
@@ -534,48 +649,78 @@ mod tests {
use tc::Fg;
use tcolor as tc;
assert_eq!(Style::from(Fg(tc::Reset)), Style::new().fg(Color::Reset));
assert_eq!(Style::from(Fg(tc::Black)), Style::new().black());
assert_eq!(Style::from(Fg(tc::Red)), Style::new().red());
assert_eq!(Style::from(Fg(tc::Green)), Style::new().green());
assert_eq!(Style::from(Fg(tc::Yellow)), Style::new().yellow());
assert_eq!(Style::from(Fg(tc::Blue)), Style::default().blue());
assert_eq!(Style::from(Fg(tc::Magenta)), Style::default().magenta());
assert_eq!(Style::from(Fg(tc::Cyan)), Style::default().cyan());
assert_eq!(Style::from(Fg(tc::White)), Style::default().gray());
assert_eq!(Style::from(Fg(tc::LightBlack)), Style::new().dark_gray());
assert_eq!(Style::from(Fg(tc::LightRed)), Style::new().light_red());
assert_eq!(Style::from(Fg(tc::LightGreen)), Style::new().light_green());
assert_eq!(Style::from(Fg(tc::LightBlue)), Style::new().light_blue());
assert_eq!(
Style::from(Fg(tc::LightYellow)),
Style::from_termion(Fg(tc::Reset)),
Style::new().fg(Color::Reset)
);
assert_eq!(Style::from_termion(Fg(tc::Black)), Style::new().black());
assert_eq!(Style::from_termion(Fg(tc::Red)), Style::new().red());
assert_eq!(Style::from_termion(Fg(tc::Green)), Style::new().green());
assert_eq!(Style::from_termion(Fg(tc::Yellow)), Style::new().yellow());
assert_eq!(Style::from_termion(Fg(tc::Blue)), Style::default().blue());
assert_eq!(
Style::from_termion(Fg(tc::Magenta)),
Style::default().magenta()
);
assert_eq!(Style::from_termion(Fg(tc::Cyan)), Style::default().cyan());
assert_eq!(Style::from_termion(Fg(tc::White)), Style::default().gray());
assert_eq!(
Style::from_termion(Fg(tc::LightBlack)),
Style::new().dark_gray()
);
assert_eq!(
Style::from_termion(Fg(tc::LightRed)),
Style::new().light_red()
);
assert_eq!(
Style::from_termion(Fg(tc::LightGreen)),
Style::new().light_green()
);
assert_eq!(
Style::from_termion(Fg(tc::LightBlue)),
Style::new().light_blue()
);
assert_eq!(
Style::from_termion(Fg(tc::LightYellow)),
Style::new().light_yellow()
);
assert_eq!(
Style::from(Fg(tc::LightMagenta)),
Style::from_termion(Fg(tc::LightMagenta)),
Style::new().light_magenta()
);
assert_eq!(Style::from(Fg(tc::LightCyan)), Style::new().light_cyan());
assert_eq!(Style::from(Fg(tc::LightWhite)), Style::new().white());
assert_eq!(
Style::from(Fg(tc::AnsiValue(31))),
Style::from_termion(Fg(tc::LightCyan)),
Style::new().light_cyan()
);
assert_eq!(
Style::from_termion(Fg(tc::LightWhite)),
Style::new().white()
);
assert_eq!(
Style::from_termion(Fg(tc::AnsiValue(31))),
Style::default().fg(Color::Indexed(31))
);
assert_eq!(
Style::from(Fg(tc::Rgb(1, 2, 3))),
Style::from_termion(Fg(tc::Rgb(1, 2, 3))),
Style::default().fg(Color::Rgb(1, 2, 3))
);
}
#[test]
fn from_termion_style() {
assert_eq!(Modifier::from(tstyle::Invert), Modifier::REVERSED);
assert_eq!(Modifier::from(tstyle::Bold), Modifier::BOLD);
assert_eq!(Modifier::from(tstyle::Italic), Modifier::ITALIC);
assert_eq!(Modifier::from(tstyle::Underline), Modifier::UNDERLINED);
assert_eq!(Modifier::from(tstyle::Faint), Modifier::DIM);
assert_eq!(Modifier::from(tstyle::CrossedOut), Modifier::CROSSED_OUT);
assert_eq!(Modifier::from(tstyle::Blink), Modifier::SLOW_BLINK);
assert_eq!(Modifier::from(tstyle::Reset), Modifier::empty());
assert_eq!(Modifier::from_termion(tstyle::Invert), Modifier::REVERSED);
assert_eq!(Modifier::from_termion(tstyle::Bold), Modifier::BOLD);
assert_eq!(Modifier::from_termion(tstyle::Italic), Modifier::ITALIC);
assert_eq!(
Modifier::from_termion(tstyle::Underline),
Modifier::UNDERLINED
);
assert_eq!(Modifier::from_termion(tstyle::Faint), Modifier::DIM);
assert_eq!(
Modifier::from_termion(tstyle::CrossedOut),
Modifier::CROSSED_OUT
);
assert_eq!(Modifier::from_termion(tstyle::Blink), Modifier::SLOW_BLINK);
assert_eq!(Modifier::from_termion(tstyle::Reset), Modifier::empty());
}
}

View File

@@ -0,0 +1,39 @@
[package]
name = "ratatui-termwiz"
version = "0.1.0-alpha.0"
description = "Termwiz backend for the Ratatui Terminal UI library."
documentation = "https://docs.rs/ratatui-termwiz/"
readme = "README.md"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
keywords.workspace = true
categories.workspace = true
license.workspace = true
exclude.workspace = true
edition.workspace = true
rust-version.workspace = true
[package.metadata.docs.rs]
all-features = true
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
rustdoc-args = ["--cfg", "docsrs"]
[features]
default = []
## Enables the backend code that sets the underline color.
## Underline color is not supported on Windows 7.
underline-color = []
## Use terminal scrolling regions to make Terminal::insert_before less prone to flickering.
scrolling-regions = ["ratatui-core/scrolling-regions"]
[dependencies]
document-features = { workspace = true, optional = true }
ratatui-core = { workspace = true }
termwiz.workspace = true
[dev-dependencies]
ratatui = { path = "../ratatui", features = ["termwiz"] }
rstest.workspace = true

11
ratatui-termwiz/README.md Normal file
View File

@@ -0,0 +1,11 @@
# Ratatui-termwiz
<!-- cargo-rdme start -->
This module provides the [`TermwizBackend`] implementation for the [`Backend`] trait. It uses
the [Termwiz] crate to interact with the terminal.
[`Backend`]: trait.Backend.html
[Termwiz]: https://crates.io/crates/termwiz
<!-- cargo-rdme end -->

858
ratatui-termwiz/src/lib.rs Normal file
View File

@@ -0,0 +1,858 @@
// show the feature flags in the generated documentation
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png",
html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico"
)]
#![warn(missing_docs)]
//! This module provides the [`TermwizBackend`] implementation for the [`Backend`] trait. It uses
//! the [Termwiz] crate to interact with the terminal.
//!
//! [`Backend`]: trait.Backend.html
//! [Termwiz]: https://crates.io/crates/termwiz
#![cfg_attr(feature = "document-features", doc = "\n## Features")]
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
use std::{error::Error, io};
use ratatui_core::{
backend::{Backend, WindowSize},
buffer::Cell,
layout::{Position, Size},
style::{Color, Modifier, Style},
};
pub use termwiz;
use termwiz::{
caps::Capabilities,
cell::{AttributeChange, Blink, CellAttributes, Intensity, Underline},
color::{AnsiColor, ColorAttribute, ColorSpec, LinearRgba, RgbColor, SrgbaTuple},
surface::{Change, CursorVisibility, Position as TermwizPosition},
terminal::{buffered::BufferedTerminal, ScreenSize, SystemTerminal, Terminal},
};
/// A [`Backend`] implementation that uses [Termwiz] to render to the terminal.
///
/// The `TermwizBackend` struct is a wrapper around a [`BufferedTerminal`], which is used to send
/// commands to the terminal. It provides methods for drawing content, manipulating the cursor, and
/// clearing the terminal screen.
///
/// Most applications should not call the methods on `TermwizBackend` directly, but will instead
/// use the [`Terminal`] struct, which provides a more ergonomic interface.
///
/// This backend automatically enables raw mode and switches to the alternate screen when it is
/// created using the [`TermwizBackend::new`] method (and disables raw mode and returns to the main
/// screen when dropped). Use the [`TermwizBackend::with_buffered_terminal`] to create a new
/// instance with a custom [`BufferedTerminal`] if this is not desired.
///
/// # Example
///
/// ```rust,no_run
/// use ratatui::{backend::TermwizBackend, Terminal};
///
/// let backend = TermwizBackend::new()?;
/// let mut terminal = Terminal::new(backend)?;
///
/// terminal.clear()?;
/// terminal.draw(|frame| {
/// // -- snip --
/// })?;
/// # std::result::Result::Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// See the the [Examples] directory for more examples. See the [`backend`] module documentation
/// for more details on raw mode and alternate screen.
///
/// [`backend`]: ratatui_core::backend
/// [`Terminal`]: https://docs.rs/ratatui/latest/ratatui/struct.Terminal.html
/// [`BufferedTerminal`]: termwiz::terminal::buffered::BufferedTerminal
/// [Termwiz]: https://crates.io/crates/termwiz
/// [Examples]: https://github.com/ratatui/ratatui/tree/main/ratatui/examples/README.md
pub struct TermwizBackend {
buffered_terminal: BufferedTerminal<SystemTerminal>,
}
impl TermwizBackend {
/// Creates a new Termwiz backend instance.
///
/// The backend will automatically enable raw mode and enter the alternate screen.
///
/// # Errors
///
/// Returns an error if unable to do any of the following:
/// - query the terminal capabilities.
/// - enter raw mode.
/// - enter the alternate screen.
/// - create the system or buffered terminal.
///
/// # Example
///
/// ```rust,no_run
/// use ratatui::backend::TermwizBackend;
///
/// let backend = TermwizBackend::new()?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
pub fn new() -> Result<Self, Box<dyn Error>> {
let mut buffered_terminal =
BufferedTerminal::new(SystemTerminal::new(Capabilities::new_from_env()?)?)?;
buffered_terminal.terminal().set_raw_mode()?;
buffered_terminal.terminal().enter_alternate_screen()?;
Ok(Self { buffered_terminal })
}
/// Creates a new Termwiz backend instance with the given buffered terminal.
pub const fn with_buffered_terminal(instance: BufferedTerminal<SystemTerminal>) -> Self {
Self {
buffered_terminal: instance,
}
}
/// Returns a reference to the buffered terminal used by the backend.
pub const fn buffered_terminal(&self) -> &BufferedTerminal<SystemTerminal> {
&self.buffered_terminal
}
/// Returns a mutable reference to the buffered terminal used by the backend.
pub fn buffered_terminal_mut(&mut self) -> &mut BufferedTerminal<SystemTerminal> {
&mut self.buffered_terminal
}
}
impl Backend for TermwizBackend {
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
for (x, y, cell) in content {
self.buffered_terminal.add_changes(vec![
Change::CursorPosition {
x: TermwizPosition::Absolute(x as usize),
y: TermwizPosition::Absolute(y as usize),
},
Change::Attribute(AttributeChange::Foreground(cell.fg.into_termwiz())),
Change::Attribute(AttributeChange::Background(cell.bg.into_termwiz())),
]);
self.buffered_terminal
.add_change(Change::Attribute(AttributeChange::Intensity(
if cell.modifier.contains(Modifier::BOLD) {
Intensity::Bold
} else if cell.modifier.contains(Modifier::DIM) {
Intensity::Half
} else {
Intensity::Normal
},
)));
self.buffered_terminal
.add_change(Change::Attribute(AttributeChange::Italic(
cell.modifier.contains(Modifier::ITALIC),
)));
self.buffered_terminal
.add_change(Change::Attribute(AttributeChange::Underline(
if cell.modifier.contains(Modifier::UNDERLINED) {
Underline::Single
} else {
Underline::None
},
)));
self.buffered_terminal
.add_change(Change::Attribute(AttributeChange::Reverse(
cell.modifier.contains(Modifier::REVERSED),
)));
self.buffered_terminal
.add_change(Change::Attribute(AttributeChange::Invisible(
cell.modifier.contains(Modifier::HIDDEN),
)));
self.buffered_terminal
.add_change(Change::Attribute(AttributeChange::StrikeThrough(
cell.modifier.contains(Modifier::CROSSED_OUT),
)));
self.buffered_terminal
.add_change(Change::Attribute(AttributeChange::Blink(
if cell.modifier.contains(Modifier::SLOW_BLINK) {
Blink::Slow
} else if cell.modifier.contains(Modifier::RAPID_BLINK) {
Blink::Rapid
} else {
Blink::None
},
)));
self.buffered_terminal.add_change(cell.symbol());
}
Ok(())
}
fn hide_cursor(&mut self) -> io::Result<()> {
self.buffered_terminal
.add_change(Change::CursorVisibility(CursorVisibility::Hidden));
Ok(())
}
fn show_cursor(&mut self) -> io::Result<()> {
self.buffered_terminal
.add_change(Change::CursorVisibility(CursorVisibility::Visible));
Ok(())
}
fn get_cursor_position(&mut self) -> io::Result<Position> {
let (x, y) = self.buffered_terminal.cursor_position();
Ok(Position::new(x as u16, y as u16))
}
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
let Position { x, y } = position.into();
self.buffered_terminal.add_change(Change::CursorPosition {
x: TermwizPosition::Absolute(x as usize),
y: TermwizPosition::Absolute(y as usize),
});
Ok(())
}
fn clear(&mut self) -> io::Result<()> {
self.buffered_terminal
.add_change(Change::ClearScreen(termwiz::color::ColorAttribute::Default));
Ok(())
}
fn size(&self) -> io::Result<Size> {
let (cols, rows) = self.buffered_terminal.dimensions();
Ok(Size::new(u16_max(cols), u16_max(rows)))
}
fn window_size(&mut self) -> io::Result<WindowSize> {
let ScreenSize {
cols,
rows,
xpixel,
ypixel,
} = self
.buffered_terminal
.terminal()
.get_screen_size()
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
Ok(WindowSize {
columns_rows: Size {
width: u16_max(cols),
height: u16_max(rows),
},
pixels: Size {
width: u16_max(xpixel),
height: u16_max(ypixel),
},
})
}
fn flush(&mut self) -> io::Result<()> {
self.buffered_terminal
.flush()
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
Ok(())
}
#[cfg(feature = "scrolling-regions")]
fn scroll_region_up(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
// termwiz doesn't have a command to just set the scrolling region. Instead, setting the
// scrolling region and scrolling are combined. However, this has the side-effect of
// leaving the scrolling region set. To reset the scrolling region, termwiz advises one to
// make a scrolling-region scroll command that contains the entire screen, but scrolls by 0
// lines. See [`Change::ScrollRegionUp`] for more details.
let (_, rows) = self.buffered_terminal.dimensions();
self.buffered_terminal.add_changes(vec![
Change::ScrollRegionUp {
first_row: region.start as usize,
region_size: region.len(),
scroll_count: amount as usize,
},
Change::ScrollRegionUp {
first_row: 0,
region_size: rows,
scroll_count: 0,
},
]);
Ok(())
}
#[cfg(feature = "scrolling-regions")]
fn scroll_region_down(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
// termwiz doesn't have a command to just set the scrolling region. Instead, setting the
// scrolling region and scrolling are combined. However, this has the side-effect of
// leaving the scrolling region set. To reset the scrolling region, termwiz advises one to
// make a scrolling-region scroll command that contains the entire screen, but scrolls by 0
// lines. See [`Change::ScrollRegionDown`] for more details.
let (_, rows) = self.buffered_terminal.dimensions();
self.buffered_terminal.add_changes(vec![
Change::ScrollRegionDown {
first_row: region.start as usize,
region_size: region.len(),
scroll_count: amount as usize,
},
Change::ScrollRegionDown {
first_row: 0,
region_size: rows,
scroll_count: 0,
},
]);
Ok(())
}
}
/// A trait for converting types from Termwiz to Ratatui.
///
/// This trait replaces the `From` trait for converting types from Termwiz to Ratatui. It is
/// necessary because the `From` trait is not implemented for types defined in external crates.
pub trait FromTermwiz<T> {
/// Converts the given Termwiz type to the Ratatui type.
fn from_termwiz(termwiz: T) -> Self;
}
/// A trait for converting types from Ratatui to Termwiz.
///
/// This trait replaces the `Into` trait for converting types from Ratatui to Termwiz. It is
/// necessary because the `Into` trait is not implemented for types defined in external crates.
pub trait IntoTermwiz<T> {
/// Converts the given Ratatui type to the Termwiz type.
fn into_termwiz(self) -> T;
}
/// A replacement for the `Into` trait for converting types from Ratatui to Termwiz.
///
/// This trait is necessary because the `Into` trait is not implemented for types defined in
/// external crates.
///
/// A blanket implementation is provided for all types that implement `FromTermwiz`.
///
/// This trait is private to the module as it would otherwise conflict with the other backend
/// modules. It is mainly used to avoid rewriting all the `.into()` calls in this module.
trait IntoRatatui<R> {
fn into_ratatui(self) -> R;
}
impl<C, R: FromTermwiz<C>> IntoRatatui<R> for C {
fn into_ratatui(self) -> R {
R::from_termwiz(self)
}
}
impl FromTermwiz<CellAttributes> for Style {
fn from_termwiz(value: CellAttributes) -> Self {
let mut style = Self::new()
.add_modifier(value.intensity().into_ratatui())
.add_modifier(value.underline().into_ratatui())
.add_modifier(value.blink().into_ratatui());
if value.italic() {
style.add_modifier |= Modifier::ITALIC;
}
if value.reverse() {
style.add_modifier |= Modifier::REVERSED;
}
if value.strikethrough() {
style.add_modifier |= Modifier::CROSSED_OUT;
}
if value.invisible() {
style.add_modifier |= Modifier::HIDDEN;
}
style.fg = Some(value.foreground().into_ratatui());
style.bg = Some(value.background().into_ratatui());
#[cfg(feature = "underline-color")]
{
style.underline_color = Some(value.underline_color().into_ratatui());
}
style
}
}
impl FromTermwiz<Intensity> for Modifier {
fn from_termwiz(value: Intensity) -> Self {
match value {
Intensity::Normal => Self::empty(),
Intensity::Bold => Self::BOLD,
Intensity::Half => Self::DIM,
}
}
}
impl FromTermwiz<Underline> for Modifier {
fn from_termwiz(value: Underline) -> Self {
match value {
Underline::None => Self::empty(),
_ => Self::UNDERLINED,
}
}
}
impl FromTermwiz<Blink> for Modifier {
fn from_termwiz(value: Blink) -> Self {
match value {
Blink::None => Self::empty(),
Blink::Slow => Self::SLOW_BLINK,
Blink::Rapid => Self::RAPID_BLINK,
}
}
}
impl IntoTermwiz<ColorAttribute> for Color {
fn into_termwiz(self) -> ColorAttribute {
match self {
Self::Reset => ColorAttribute::Default,
Self::Black => AnsiColor::Black.into(),
Self::DarkGray => AnsiColor::Grey.into(),
Self::Gray => AnsiColor::Silver.into(),
Self::Red => AnsiColor::Maroon.into(),
Self::LightRed => AnsiColor::Red.into(),
Self::Green => AnsiColor::Green.into(),
Self::LightGreen => AnsiColor::Lime.into(),
Self::Yellow => AnsiColor::Olive.into(),
Self::LightYellow => AnsiColor::Yellow.into(),
Self::Magenta => AnsiColor::Purple.into(),
Self::LightMagenta => AnsiColor::Fuchsia.into(),
Self::Cyan => AnsiColor::Teal.into(),
Self::LightCyan => AnsiColor::Aqua.into(),
Self::White => AnsiColor::White.into(),
Self::Blue => AnsiColor::Navy.into(),
Self::LightBlue => AnsiColor::Blue.into(),
Self::Indexed(i) => ColorAttribute::PaletteIndex(i),
Self::Rgb(r, g, b) => {
ColorAttribute::TrueColorWithDefaultFallback(SrgbaTuple::from((r, g, b)))
}
}
}
}
impl FromTermwiz<AnsiColor> for Color {
fn from_termwiz(value: AnsiColor) -> Self {
match value {
AnsiColor::Black => Self::Black,
AnsiColor::Grey => Self::DarkGray,
AnsiColor::Silver => Self::Gray,
AnsiColor::Maroon => Self::Red,
AnsiColor::Red => Self::LightRed,
AnsiColor::Green => Self::Green,
AnsiColor::Lime => Self::LightGreen,
AnsiColor::Olive => Self::Yellow,
AnsiColor::Yellow => Self::LightYellow,
AnsiColor::Purple => Self::Magenta,
AnsiColor::Fuchsia => Self::LightMagenta,
AnsiColor::Teal => Self::Cyan,
AnsiColor::Aqua => Self::LightCyan,
AnsiColor::White => Self::White,
AnsiColor::Navy => Self::Blue,
AnsiColor::Blue => Self::LightBlue,
}
}
}
impl FromTermwiz<ColorAttribute> for Color {
fn from_termwiz(value: ColorAttribute) -> Self {
match value {
ColorAttribute::TrueColorWithDefaultFallback(srgba)
| ColorAttribute::TrueColorWithPaletteFallback(srgba, _) => srgba.into_ratatui(),
ColorAttribute::PaletteIndex(i) => Self::Indexed(i),
ColorAttribute::Default => Self::Reset,
}
}
}
impl FromTermwiz<ColorSpec> for Color {
fn from_termwiz(value: ColorSpec) -> Self {
match value {
ColorSpec::Default => Self::Reset,
ColorSpec::PaletteIndex(i) => Self::Indexed(i),
ColorSpec::TrueColor(srgba) => srgba.into_ratatui(),
}
}
}
impl FromTermwiz<SrgbaTuple> for Color {
fn from_termwiz(value: SrgbaTuple) -> Self {
let (r, g, b, _) = value.to_srgb_u8();
Self::Rgb(r, g, b)
}
}
impl FromTermwiz<RgbColor> for Color {
fn from_termwiz(value: RgbColor) -> Self {
let (r, g, b) = value.to_tuple_rgb8();
Self::Rgb(r, g, b)
}
}
impl FromTermwiz<LinearRgba> for Color {
fn from_termwiz(value: LinearRgba) -> Self {
value.to_srgb().into_ratatui()
}
}
#[inline]
fn u16_max(i: usize) -> u16 {
u16::try_from(i).unwrap_or(u16::MAX)
}
#[cfg(test)]
mod tests {
use super::*;
mod into_color {
use Color as C;
use super::*;
#[test]
fn from_linear_rgba() {
// full black + opaque
assert_eq!(
C::from_termwiz(LinearRgba(0., 0., 0., 1.)),
Color::Rgb(0, 0, 0)
);
// full black + transparent
assert_eq!(
C::from_termwiz(LinearRgba(0., 0., 0., 0.)),
Color::Rgb(0, 0, 0)
);
// full white + opaque
assert_eq!(
C::from_termwiz(LinearRgba(1., 1., 1., 1.)),
C::Rgb(254, 254, 254)
);
// full white + transparent
assert_eq!(
C::from_termwiz(LinearRgba(1., 1., 1., 0.)),
C::Rgb(254, 254, 254)
);
// full red
assert_eq!(
C::from_termwiz(LinearRgba(1., 0., 0., 1.)),
C::Rgb(254, 0, 0)
);
// full green
assert_eq!(
C::from_termwiz(LinearRgba(0., 1., 0., 1.)),
C::Rgb(0, 254, 0)
);
// full blue
assert_eq!(
C::from_termwiz(LinearRgba(0., 0., 1., 1.)),
C::Rgb(0, 0, 254)
);
// See https://stackoverflow.com/questions/12524623/what-are-the-practical-differences-when-working-with-colors-in-a-linear-vs-a-no
// for an explanation
// half red
assert_eq!(
C::from_termwiz(LinearRgba(0.214, 0., 0., 1.)),
C::Rgb(127, 0, 0)
);
// half green
assert_eq!(
C::from_termwiz(LinearRgba(0., 0.214, 0., 1.)),
C::Rgb(0, 127, 0)
);
// half blue
assert_eq!(
C::from_termwiz(LinearRgba(0., 0., 0.214, 1.)),
C::Rgb(0, 0, 127)
);
}
#[test]
fn from_srgba() {
// full black + opaque
assert_eq!(
C::from_termwiz(SrgbaTuple(0., 0., 0., 1.)),
Color::Rgb(0, 0, 0)
);
// full black + transparent
assert_eq!(
C::from_termwiz(SrgbaTuple(0., 0., 0., 0.)),
Color::Rgb(0, 0, 0)
);
// full white + opaque
assert_eq!(
C::from_termwiz(SrgbaTuple(1., 1., 1., 1.)),
C::Rgb(255, 255, 255)
);
// full white + transparent
assert_eq!(
C::from_termwiz(SrgbaTuple(1., 1., 1., 0.)),
C::Rgb(255, 255, 255)
);
// full red
assert_eq!(
C::from_termwiz(SrgbaTuple(1., 0., 0., 1.)),
C::Rgb(255, 0, 0)
);
// full green
assert_eq!(
C::from_termwiz(SrgbaTuple(0., 1., 0., 1.)),
C::Rgb(0, 255, 0)
);
// full blue
assert_eq!(
C::from_termwiz(SrgbaTuple(0., 0., 1., 1.)),
C::Rgb(0, 0, 255)
);
// half red
assert_eq!(
C::from_termwiz(SrgbaTuple(0.5, 0., 0., 1.)),
C::Rgb(127, 0, 0)
);
// half green
assert_eq!(
C::from_termwiz(SrgbaTuple(0., 0.5, 0., 1.)),
C::Rgb(0, 127, 0)
);
// half blue
assert_eq!(
C::from_termwiz(SrgbaTuple(0., 0., 0.5, 1.)),
C::Rgb(0, 0, 127)
);
}
#[test]
fn from_rgbcolor() {
// full black
assert_eq!(
C::from_termwiz(RgbColor::new_8bpc(0, 0, 0)),
Color::Rgb(0, 0, 0)
);
// full white
assert_eq!(
C::from_termwiz(RgbColor::new_8bpc(255, 255, 255)),
C::Rgb(255, 255, 255)
);
// full red
assert_eq!(
C::from_termwiz(RgbColor::new_8bpc(255, 0, 0)),
C::Rgb(255, 0, 0)
);
// full green
assert_eq!(
C::from_termwiz(RgbColor::new_8bpc(0, 255, 0)),
C::Rgb(0, 255, 0)
);
// full blue
assert_eq!(
C::from_termwiz(RgbColor::new_8bpc(0, 0, 255)),
C::Rgb(0, 0, 255)
);
// half red
assert_eq!(
C::from_termwiz(RgbColor::new_8bpc(127, 0, 0)),
C::Rgb(127, 0, 0)
);
// half green
assert_eq!(
C::from_termwiz(RgbColor::new_8bpc(0, 127, 0)),
C::Rgb(0, 127, 0)
);
// half blue
assert_eq!(
C::from_termwiz(RgbColor::new_8bpc(0, 0, 127)),
C::Rgb(0, 0, 127)
);
}
#[test]
fn from_colorspec() {
assert_eq!(C::from_termwiz(ColorSpec::Default), C::Reset);
assert_eq!(C::from_termwiz(ColorSpec::PaletteIndex(33)), C::Indexed(33));
assert_eq!(
C::from_termwiz(ColorSpec::TrueColor(SrgbaTuple(0., 0., 0., 1.))),
C::Rgb(0, 0, 0)
);
}
#[test]
fn from_colorattribute() {
assert_eq!(C::from_termwiz(ColorAttribute::Default), C::Reset);
assert_eq!(
C::from_termwiz(ColorAttribute::PaletteIndex(32)),
C::Indexed(32)
);
assert_eq!(
C::from_termwiz(ColorAttribute::TrueColorWithDefaultFallback(SrgbaTuple(
0., 0., 0., 1.
))),
C::Rgb(0, 0, 0)
);
assert_eq!(
C::from_termwiz(ColorAttribute::TrueColorWithPaletteFallback(
SrgbaTuple(0., 0., 0., 1.),
31
)),
C::Rgb(0, 0, 0)
);
}
#[test]
fn from_ansicolor() {
assert_eq!(C::from_termwiz(AnsiColor::Black), Color::Black);
assert_eq!(C::from_termwiz(AnsiColor::Grey), Color::DarkGray);
assert_eq!(C::from_termwiz(AnsiColor::Silver), Color::Gray);
assert_eq!(C::from_termwiz(AnsiColor::Maroon), Color::Red);
assert_eq!(C::from_termwiz(AnsiColor::Red), Color::LightRed);
assert_eq!(C::from_termwiz(AnsiColor::Green), Color::Green);
assert_eq!(C::from_termwiz(AnsiColor::Lime), Color::LightGreen);
assert_eq!(C::from_termwiz(AnsiColor::Olive), Color::Yellow);
assert_eq!(C::from_termwiz(AnsiColor::Yellow), Color::LightYellow);
assert_eq!(C::from_termwiz(AnsiColor::Purple), Color::Magenta);
assert_eq!(C::from_termwiz(AnsiColor::Fuchsia), Color::LightMagenta);
assert_eq!(C::from_termwiz(AnsiColor::Teal), Color::Cyan);
assert_eq!(C::from_termwiz(AnsiColor::Aqua), Color::LightCyan);
assert_eq!(C::from_termwiz(AnsiColor::White), Color::White);
assert_eq!(C::from_termwiz(AnsiColor::Navy), Color::Blue);
assert_eq!(C::from_termwiz(AnsiColor::Blue), Color::LightBlue);
}
}
mod into_modifier {
use super::*;
#[test]
fn from_intensity() {
assert_eq!(Modifier::from_termwiz(Intensity::Normal), Modifier::empty());
assert_eq!(Modifier::from_termwiz(Intensity::Bold), Modifier::BOLD);
assert_eq!(Modifier::from_termwiz(Intensity::Half), Modifier::DIM);
}
#[test]
fn from_underline() {
assert_eq!(Modifier::from_termwiz(Underline::None), Modifier::empty());
assert_eq!(
Modifier::from_termwiz(Underline::Single),
Modifier::UNDERLINED
);
assert_eq!(
Modifier::from_termwiz(Underline::Double),
Modifier::UNDERLINED
);
assert_eq!(
Modifier::from_termwiz(Underline::Curly),
Modifier::UNDERLINED
);
assert_eq!(
Modifier::from_termwiz(Underline::Dashed),
Modifier::UNDERLINED
);
assert_eq!(
Modifier::from_termwiz(Underline::Dotted),
Modifier::UNDERLINED
);
}
#[test]
fn from_blink() {
assert_eq!(Modifier::from_termwiz(Blink::None), Modifier::empty());
assert_eq!(Modifier::from_termwiz(Blink::Slow), Modifier::SLOW_BLINK);
assert_eq!(Modifier::from_termwiz(Blink::Rapid), Modifier::RAPID_BLINK);
}
}
#[test]
fn from_cell_attribute_for_style() {
use ratatui_core::style::Stylize;
#[cfg(feature = "underline-color")]
const STYLE: Style = Style::new()
.underline_color(Color::Reset)
.fg(Color::Reset)
.bg(Color::Reset);
#[cfg(not(feature = "underline-color"))]
const STYLE: Style = Style::new().fg(Color::Reset).bg(Color::Reset);
// default
assert_eq!(Style::from_termwiz(CellAttributes::default()), STYLE);
// foreground color
assert_eq!(
Style::from_termwiz(
CellAttributes::default()
.set_foreground(ColorAttribute::PaletteIndex(31))
.to_owned()
),
STYLE.fg(Color::Indexed(31))
);
// background color
assert_eq!(
Style::from_termwiz(
CellAttributes::default()
.set_background(ColorAttribute::PaletteIndex(31))
.to_owned()
),
STYLE.bg(Color::Indexed(31))
);
// underlined
assert_eq!(
Style::from_termwiz(
CellAttributes::default()
.set_underline(Underline::Single)
.to_owned()
),
STYLE.underlined()
);
// blink
assert_eq!(
Style::from_termwiz(CellAttributes::default().set_blink(Blink::Slow).to_owned()),
STYLE.slow_blink()
);
// intensity
assert_eq!(
Style::from_termwiz(
CellAttributes::default()
.set_intensity(Intensity::Bold)
.to_owned()
),
STYLE.bold()
);
// italic
assert_eq!(
Style::from_termwiz(CellAttributes::default().set_italic(true).to_owned()),
STYLE.italic()
);
// reversed
assert_eq!(
Style::from_termwiz(CellAttributes::default().set_reverse(true).to_owned()),
STYLE.reversed()
);
// strikethrough
assert_eq!(
Style::from_termwiz(CellAttributes::default().set_strikethrough(true).to_owned()),
STYLE.crossed_out()
);
// hidden
assert_eq!(
Style::from_termwiz(CellAttributes::default().set_invisible(true).to_owned()),
STYLE.hidden()
);
// underline color
#[cfg(feature = "underline-color")]
assert_eq!(
Style::from_termwiz(
CellAttributes::default()
.set_underline_color(AnsiColor::Red)
.to_owned()
),
STYLE.underline_color(Color::Indexed(9))
);
}
}

117
ratatui-widgets/Cargo.toml Normal file
View File

@@ -0,0 +1,117 @@
[package]
name = "ratatui-widgets"
description = "A collection of Ratatui widgets for building terminal user interfaces using Ratatui."
# Note that this started at 0.3.0 as there was a previous crate using the name `ratatui-widgets`.
# <https://github.com/joshka/ratatui-widgets/issues/46>
version = "0.3.0-alpha.0"
readme = "README.md"
authors.workspace = true
documentation.workspace = true
repository.workspace = true
homepage.workspace = true
keywords.workspace = true
categories.workspace = true
license.workspace = true
exclude.workspace = true
edition.workspace = true
rust-version.workspace = true
[package.metadata.docs.rs]
all-features = true
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
rustdoc-args = ["--cfg", "docsrs"]
[features]
default = ["all-widgets"]
## 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", "ratatui-core/serde"]
#! Widgets that add dependencies are gated behind feature flags to prevent unused transitive
#! dependencies. The available features are:
## enables all widgets.
all-widgets = ["calendar"]
## enables the [`calendar`](calendar) widget module and adds a dependency on [`time`].
calendar = ["dep:time"]
## Enable all unstable features.
unstable = ["unstable-rendered-line-info"]
## Enables the [`Paragraph::line_count`](paragraph::Paragraph::line_count)
## [`Paragraph::line_width`](paragraph::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 = []
[dependencies]
bitflags.workspace = true
itertools.workspace = true
indoc.workspace = true
instability.workspace = true
ratatui-core = { workspace = true }
strum.workspace = true
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
unicode-segmentation.workspace = true
unicode-width.workspace = true
serde = { workspace = true, optional = true }
document-features = { workspace = true, optional = true }
line-clipping = "0.2.1"
[dev-dependencies]
color-eyre.workspace = true
pretty_assertions.workspace = true
ratatui = { path = "../ratatui" }
rstest.workspace = true
[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"
[[example]]
name = "barchart"
doc-scrape-examples = true
[[example]]
name = "block"
doc-scrape-examples = true

80
ratatui-widgets/README.md Normal file
View File

@@ -0,0 +1,80 @@
# Ratatui Widgets
[![Crates.io](https://img.shields.io/crates/v/ratatui-widgets)](https://crates.io/crates/ratatui-widgets)
[![Documentation](https://docs.rs/ratatui-widgets/badge.svg)](https://docs.rs/ratatui-widgets)
[![License](https://img.shields.io/crates/l/ratatui-widgets)](../LICENSE)
<!-- ⚠️ DO NOT EDIT THIS FILE DIRECTLY, EDIT lib.rs AND THEN RUN `cargo rdme` to update this file. -->
<!-- cargo-rdme start -->
**ratatui-widgets** contains all the widgets that were previously part of the [Ratatui] crate.
It is meant to be used in conjunction with `ratatui`, which provides the core functionality
for building terminal user interfaces.
[Ratatui]: https://crates.io/crates/ratatui
Most applications shouldn't need to depend directly on `ratatui-widgets`, `ratatui` crate
re-exports all the widgets from this crate. However, if you are building a widget library that
internally uses these widgets, or if you prefer finer grained dependencies, you may want to
depend on this crate rather than transitively through the `ratatui` crate.
Previously, a crate named `ratatui-widgets` was published with some formative ideas about an
eventual Ratatui framework. That crate is now move to [tui-framework-experiment], pending a new
name.
[tui-framework-experiment]: https://crates.io/crates/tui-framework-experiment
## Installation
Run the following command to add this crate to your project:
```sh
cargo add ratatui-widgets
```
## Available Widgets
- [`BarChart`]: displays multiple datasets as bars with optional grouping.
- [`Block`]: a basic widget that draws a block with optional borders, titles, and styles.
- [`calendar::Monthly`]: displays a single month.
- [`Canvas`]: draws arbitrary shapes using drawing characters.
- [`Chart`]: displays multiple datasets as lines or scatter graphs.
- [`Clear`]: clears the area it occupies. Useful to render over previously drawn widgets.
- [`Gauge`]: displays progress percentage using block characters.
- [`LineGauge`]: displays progress as a line.
- [`List`]: displays a list of items and allows selection.
- [`RatatuiLogo`]: displays the Ratatui logo.
- [`Paragraph`]: displays a paragraph of optionally styled and wrapped text.
- [`Scrollbar`]: displays a scrollbar.
- [`Sparkline`]: displays a single dataset as a sparkline.
- [`Table`]: displays multiple rows and columns in a grid and allows selection.
- [`Tabs`]: displays a tab bar and allows selection.
[`BarChart`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/barchart/struct.BarChart.html
[`Block`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/block/struct.Block.html
[`calendar::Monthly`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/calendar/struct.Monthly.html
[`Canvas`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/canvas/struct.Canvas.html
[`Chart`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/chart/struct.Chart.html
[`Clear`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/clear/struct.Clear.html
[`Gauge`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/gauge/struct.Gauge.html
[`LineGauge`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/gauge/struct.LineGauge.html
[`List`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/list/struct.List.html
[`RatatuiLogo`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/logo/struct.RatatuiLogo.html
[`Paragraph`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/paragraph/struct.Paragraph.html
[`Scrollbar`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/scrollbar/struct.Scrollbar.html
[`Sparkline`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/sparkline/struct.Sparkline.html
[`Table`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/table/struct.Table.html
[`Tabs`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/tabs/struct.Tabs.html
All these widgets are re-exported directly under `ratatui::widgets` in the `ratatui` crate.
## Contributing
Contributions are welcome! Please open an issue or submit a pull request on GitHub. For more
details on contributing, please see the [CONTRIBUTING](CONTRIBUTING.md) document.
## License
This project is licensed under the MIT License. See the [LICENSE](../LICENSE) file for details.
<!-- cargo-rdme end -->

View File

@@ -0,0 +1,91 @@
//! # [Ratatui] `BarChart` example
//!
//! The latest version of this example is available in the [widget 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
//! [widget examples]: https://github.com/ratatui/ratatui/blob/main/ratatui-widgets/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use color_eyre::Result;
use ratatui::{
layout::{Constraint, Layout, Rect},
style::Stylize,
text::{Line, Span},
widgets::{Bar, BarChart},
DefaultTerminal, Frame,
};
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let result = run(terminal);
ratatui::restore();
result
}
/// Run the application.
fn run(mut terminal: DefaultTerminal) -> Result<()> {
loop {
terminal.draw(draw)?;
if quit_key_pressed()? {
break Ok(());
}
}
}
/// Draw the UI with a title and two barcharts.
fn draw(frame: &mut Frame) {
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).spacing(1);
let horizontal = Layout::horizontal([Constraint::Length(28), Constraint::Fill(1)]).spacing(1);
let [top, main] = vertical.areas(frame.area());
let [left, right] = horizontal.areas(main);
let title = Line::from_iter([
Span::from("BarChart Widget").bold(),
Span::from(" (Press 'q' to quit)"),
]);
frame.render_widget(title.centered(), top);
render_vertical_barchart(frame, left);
render_horizontal_barchart(frame, right);
}
/// Render a horizontal barchart with some sample data.
fn render_horizontal_barchart(frame: &mut Frame, area: Rect) {
let bars = vec![
Bar::with_label("Red", 30).red(),
Bar::with_label("Blue", 20).blue(),
Bar::with_label("Green", 15).green(),
Bar::with_label("Yellow", 10).yellow(),
];
let chart = BarChart::horizontal(bars).bar_width(3);
frame.render_widget(chart, area);
}
/// Render a vertical barchart with some sample data.
fn render_vertical_barchart(frame: &mut Frame, area: Rect) {
let bars = vec![
Bar::with_label("Red", 30).red(),
Bar::with_label("Blue", 20).blue(),
Bar::with_label("Green", 15).green(),
Bar::with_label("Yellow", 10).yellow(),
];
let chart = BarChart::vertical(bars).bar_width(6);
frame.render_widget(chart, area);
}
/// Wait for an event and return `true` if the Esc or 'q' key is pressed.
fn quit_key_pressed() -> Result<bool> {
use ratatui::crossterm::event::{self, Event, KeyCode};
match event::read()? {
Event::Key(event) if matches!(event.code, KeyCode::Esc | KeyCode::Char('q')) => Ok(true),
_ => Ok(false),
}
}

View File

@@ -0,0 +1,84 @@
//! # [Ratatui] `Block` example
//!
//! The latest version of this example is available in the [widget 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
//! [widget examples]: https://github.com/ratatui/ratatui/blob/main/ratatui-widgets/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use color_eyre::Result;
use ratatui::{
crossterm::event::{self, Event},
layout::{Constraint, Layout, Rect},
style::{Style, Stylize},
text::{Line, Span},
widgets::{Block, BorderType},
DefaultTerminal, Frame,
};
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let result = run(terminal);
ratatui::restore();
result
}
/// Run the application.
fn run(mut terminal: DefaultTerminal) -> Result<()> {
loop {
terminal.draw(draw)?;
if matches!(event::read()?, Event::Key(_)) {
break Ok(());
}
}
}
/// Draw the UI with various blocks.
fn draw(frame: &mut Frame) {
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).spacing(1);
let horizontal = Layout::horizontal([Constraint::Percentage(33); 3]).spacing(1);
let [top, main] = vertical.areas(frame.area());
let [left, middle, right] = horizontal.areas(main);
let title = Line::from_iter([
Span::from("Block Widget").bold(),
Span::from(" (Press 'q' to quit)"),
]);
frame.render_widget(title.centered(), top);
render_bordered_block(frame, left);
render_styled_block(frame, middle);
render_custom_bordered_block(frame, right);
}
/// Render a block with borders.
pub fn render_bordered_block(frame: &mut Frame, area: Rect) {
let block = Block::bordered().title("Bordered block");
frame.render_widget(block, area);
}
/// Render a styled block.
pub fn render_styled_block(frame: &mut Frame, area: Rect) {
let block = Block::bordered()
.style(Style::new().blue().on_black().bold().italic())
.title("Styled block");
frame.render_widget(block, area);
}
/// Render a block with custom borders.
pub fn render_custom_bordered_block(frame: &mut Frame, area: Rect) {
let block = Block::bordered()
.border_type(BorderType::Rounded)
.border_style(Style::new().red())
.title("Custom borders");
frame.render_widget(block, area);
}

View File

@@ -1,11 +1,20 @@
use crate::{prelude::*, style::Styled, widgets::Block};
//! The [`BarChart`] widget and its related types (e.g. [`Bar`], [`BarGroup`]).
use ratatui_core::{
buffer::Buffer,
layout::{Direction, Rect},
style::{Style, Styled},
symbols::{self},
text::Line,
widgets::Widget,
};
pub use self::{bar::Bar, bar_group::BarGroup};
use crate::block::{Block, BlockExt};
mod bar;
mod bar_group;
pub use bar::Bar;
pub use bar_group::BarGroup;
/// A chart showing values as [bars](Bar).
///
/// Here is a possible `BarChart` output.
@@ -42,7 +51,10 @@ pub use bar_group::BarGroup;
/// The first group is added by an array slice (`&[(&str, u64)]`).
/// The second group is added by a [`BarGroup`] instance.
/// ```
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Style, Stylize},
/// widgets::{Bar, BarChart, BarGroup, Block},
/// };
///
/// BarChart::default()
/// .block(Block::bordered().title("BarChart"))
@@ -52,10 +64,21 @@ pub use bar_group::BarGroup;
/// .bar_style(Style::new().yellow().on_red())
/// .value_style(Style::new().red().bold())
/// .label_style(Style::new().white())
/// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
/// .data(BarGroup::default().bars(&[Bar::default().value(10), Bar::default().value(20)]))
/// .data(&[("A0", 0), ("A1", 2), ("A2", 4), ("A3", 3)])
/// .data(BarGroup::new([
/// Bar::with_label("B0", 10),
/// Bar::with_label("B2", 20),
/// ]))
/// .max(4);
/// ```
///
/// For simpler usages, you can also create a `BarChart` simply by
///
/// ```rust
/// use ratatui::widgets::{Bar, BarChart};
///
/// BarChart::new([Bar::with_label("A", 10), Bar::with_label("B", 20)]);
/// ```
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct BarChart<'a> {
/// Block to wrap the widget in
@@ -105,6 +128,52 @@ impl<'a> Default for BarChart<'a> {
}
impl<'a> BarChart<'a> {
/// Creates a new vertical `BarChart` widget with the given bars.
///
/// The `bars` parameter accepts any type that can be converted into a `Vec<Bar>`.
///
/// # Examples
///
/// ```rust
/// use ratatui::{
/// layout::Direction,
/// widgets::{Bar, BarChart},
/// };
///
/// BarChart::new(vec![Bar::with_label("A", 10), Bar::with_label("B", 10)]);
/// ```
pub fn new<T: Into<Vec<Bar<'a>>>>(bars: T) -> Self {
Self {
data: vec![BarGroup::new(bars.into())],
direction: Direction::Vertical,
..Default::default()
}
}
/// Creates a new `BarChart` widget with a vertical direction.
///
/// This function is equivalent to `BarChart::new()`.
pub fn vertical(bars: impl Into<Vec<Bar<'a>>>) -> Self {
Self::new(bars)
}
/// Creates a new `BarChart` widget with a horizontal direction.
///
/// # Examples
///
/// ```rust
/// use ratatui::widgets::{Bar, BarChart};
///
/// BarChart::horizontal(vec![Bar::with_label("A", 10), Bar::with_label("B", 20)]);
/// ```
pub fn horizontal(bars: impl Into<Vec<Bar<'a>>>) -> Self {
Self {
data: vec![BarGroup::new(bars.into())],
direction: Direction::Horizontal,
..Default::default()
}
}
/// Add group of bars to the `BarChart`
///
/// # Examples
@@ -113,10 +182,14 @@ impl<'a> BarChart<'a> {
/// The first group is added by an array slice (`&[(&str, u64)]`).
/// The second group is added by a [`BarGroup`] instance.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::{Bar, BarChart, BarGroup};
///
/// BarChart::default()
/// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
/// .data(BarGroup::default().bars(&[Bar::default().value(10), Bar::default().value(20)]));
/// .data(BarGroup::new([
/// Bar::with_label("A", 10),
/// Bar::with_label("B", 20),
/// ]));
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn data(mut self, data: impl Into<BarGroup<'a>>) -> Self {
@@ -143,7 +216,7 @@ impl<'a> BarChart<'a> {
/// This example shows the default behavior when `max` is not set.
/// The maximum value in the dataset is taken (here, `100`).
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::BarChart;
/// BarChart::default().data(&[("foo", 1), ("bar", 2), ("baz", 100)]);
/// // Renders
/// // █
@@ -154,7 +227,8 @@ impl<'a> BarChart<'a> {
/// This example shows a custom max value.
/// The maximum height being `2`, `bar` & `baz` render as the max.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::BarChart;
///
/// BarChart::default()
/// .data(&[("foo", 1), ("bar", 2), ("baz", 100)])
/// .max(2);
@@ -176,6 +250,8 @@ impl<'a> BarChart<'a> {
///
/// It is also possible to set individually the style of each [`Bar`].
/// In this case the default style will be patched by the individual style
///
/// [`Color`]: ratatui_core::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn bar_style<S: Into<Style>>(mut self, style: S) -> Self {
self.bar_style = style.into();
@@ -184,8 +260,8 @@ impl<'a> BarChart<'a> {
/// Set the width of the displayed bars.
///
/// For [`Horizontal`](crate::layout::Direction::Horizontal) bars this becomes the height of
/// the bar.
/// For [`Horizontal`](ratatui_core::layout::Direction::Horizontal) bars this becomes the height
/// of the bar.
///
/// If not set, this defaults to `1`.
/// The bar label also uses this value as its width.
@@ -204,7 +280,8 @@ impl<'a> BarChart<'a> {
///
/// This shows two bars with a gap of `3`. Notice the labels will always stay under the bar.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::BarChart;
///
/// BarChart::default()
/// .data(&[("foo", 1), ("bar", 2)])
/// .bar_gap(3);
@@ -219,9 +296,9 @@ impl<'a> BarChart<'a> {
self
}
/// The [`bar::Set`](crate::symbols::bar::Set) to use for displaying the bars.
/// The [`bar::Set`](ratatui_core::symbols::bar::Set) to use for displaying the bars.
///
/// If not set, the default is [`bar::NINE_LEVELS`](crate::symbols::bar::NINE_LEVELS).
/// If not set, the default is [`bar::NINE_LEVELS`](ratatui_core::symbols::bar::NINE_LEVELS).
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn bar_set(mut self, bar_set: symbols::bar::Set) -> Self {
self.bar_set = bar_set;
@@ -239,6 +316,8 @@ impl<'a> BarChart<'a> {
/// # See also
///
/// [`Bar::value_style`] to set the value style individually.
///
/// [`Color`]: ratatui_core::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn value_style<S: Into<Style>>(mut self, style: S) -> Self {
self.value_style = style.into();
@@ -256,6 +335,8 @@ impl<'a> BarChart<'a> {
/// # See also
///
/// [`Bar::label`] to set the label style individually.
///
/// [`Color`]: ratatui_core::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn label_style<S: Into<Style>>(mut self, style: S) -> Self {
self.label_style = style.into();
@@ -275,6 +356,8 @@ impl<'a> BarChart<'a> {
/// your own type that implements [`Into<Style>`]).
///
/// The style will be applied to everything that isn't styled (borders, bars, labels, ...).
///
/// [`Color`]: ratatui_core::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
@@ -283,7 +366,7 @@ impl<'a> BarChart<'a> {
/// Set the direction of the bars.
///
/// [`Vertical`](crate::layout::Direction::Vertical) bars are the default.
/// [`Vertical`](ratatui_core::layout::Direction::Vertical) bars are the default.
///
/// # Examples
///
@@ -577,15 +660,15 @@ impl BarChart<'_> {
impl Widget for BarChart<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
Widget::render(&self, area, buf);
}
}
impl WidgetRef for BarChart<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
impl Widget for &BarChart<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
self.block.render_ref(area, buf);
self.block.as_ref().render(area, buf);
let inner = self.block.inner_if_some(area);
if inner.is_empty() || self.data.is_empty() || self.bar_width == 0 {
@@ -613,9 +696,14 @@ impl<'a> Styled for BarChart<'a> {
#[cfg(test)]
mod tests {
use itertools::iproduct;
use ratatui_core::{
layout::Alignment,
style::{Color, Modifier, Stylize},
text::Span,
};
use super::*;
use crate::widgets::BorderType;
use crate::borders::BorderType;
#[test]
fn default() {
@@ -846,10 +934,10 @@ mod tests {
#[test]
fn test_empty_group() {
let chart = BarChart::default()
.data(BarGroup::default().label("invisible".into()))
.data(BarGroup::default().label("invisible"))
.data(
BarGroup::default()
.label("G".into())
.label("G")
.bars(&[Bar::default().value(1), Bar::default().value(2)]),
);
@@ -866,12 +954,12 @@ mod tests {
fn build_test_barchart<'a>() -> BarChart<'a> {
BarChart::default()
.data(BarGroup::default().label("G1".into()).bars(&[
.data(BarGroup::default().label("G1").bars(&[
Bar::default().value(2),
Bar::default().value(3),
Bar::default().value(4),
]))
.data(BarGroup::default().label("G2".into()).bars(&[
.data(BarGroup::default().label("G2").bars(&[
Bar::default().value(3),
Bar::default().value(4),
Bar::default().value(5),
@@ -938,7 +1026,7 @@ mod tests {
fn test_horizontal_bars_label_width_greater_than_bar(bar_color: Option<Color>) {
let mut bar = Bar::default()
.value(2)
.text_value("label".into())
.text_value("label")
.value_style(Style::default().red());
if let Some(color) = bar_color {
@@ -1013,7 +1101,7 @@ mod tests {
let chart: BarChart<'_> = BarChart::default()
.data(
BarGroup::default()
.label(Span::from("G1").red().into())
.label(Span::from("G1").red())
.bars(&[Bar::default().value(2)]),
)
.group_gap(1)
@@ -1081,18 +1169,9 @@ mod tests {
#[test]
fn test_unicode_as_value() {
let group = BarGroup::default().bars(&[
Bar::default()
.value(123)
.label("B1".into())
.text_value("".into()),
Bar::default()
.value(321)
.label("B2".into())
.text_value("".into()),
Bar::default()
.value(333)
.label("B2".into())
.text_value("".into()),
Bar::default().value(123).label("B1").text_value(""),
Bar::default().value(321).label("B2").text_value(""),
Bar::default().value(333).label("B2").text_value(""),
]);
let chart = BarChart::default().data(group).bar_width(3).bar_gap(1);
@@ -1134,7 +1213,7 @@ mod tests {
("i", 8),
])
.into();
group = group.label("Group".into());
group = group.label("Group");
let chart = BarChart::default()
.data(group)
@@ -1159,7 +1238,7 @@ mod tests {
("i", 8),
])
.into();
group = group.label("Group".into());
group = group.label("Group");
let chart = BarChart::default()
.data(group)
@@ -1326,4 +1405,19 @@ mod tests {
]);
assert_eq!(buffer, expected);
}
#[test]
fn test_barchart_new() {
let bars = [Bar::with_label("Red", 1), Bar::with_label("Green", 2)];
let chart = BarChart::new(bars.clone());
assert_eq!(chart.data.len(), 1);
assert_eq!(chart.data[0].bars, bars);
let bars2 = [("Blue", 3)];
let updated_chart = chart.data(&bars2);
assert_eq!(updated_chart.data.len(), 2);
assert_eq!(updated_chart.data[1].bars, [Bar::with_label("Blue", 3)]);
}
}

View File

@@ -1,8 +1,13 @@
use ratatui_core::{
buffer::Buffer,
layout::Rect,
style::{Style, Styled},
text::Line,
widgets::Widget,
};
use unicode_width::UnicodeWidthStr;
use crate::prelude::*;
/// A bar to be shown by the [`BarChart`](crate::widgets::BarChart) widget.
/// A bar to be shown by the [`BarChart`](super::BarChart) widget.
///
/// Here is an explanation of a `Bar`'s components.
/// ```plain
@@ -17,14 +22,15 @@ use crate::prelude::*;
/// The following example creates a bar with the label "Bar 1", a value "10",
/// red background and a white value foreground.
/// ```
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Style, Stylize},
/// widgets::Bar,
/// };
///
/// Bar::default()
/// .label("Bar 1".into())
/// .value(10)
/// .style(Style::default().fg(Color::Red))
/// .value_style(Style::default().bg(Color::Red).fg(Color::White))
/// .text_value("10°C".to_string());
/// Bar::with_label("Bar 1", 10)
/// .red()
/// .value_style(Style::new().red().on_white())
/// .text_value("10°C");
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Bar<'a> {
@@ -41,14 +47,54 @@ pub struct Bar<'a> {
}
impl<'a> Bar<'a> {
/// Creates a new `Bar` with the given value.
///
/// # Examples
///
/// ```
/// use ratatui::widgets::Bar;
///
/// let bar = Bar::new(42);
/// ```
pub const fn new(value: u64) -> Self {
Self {
value,
label: None,
style: Style::new(),
value_style: Style::new(),
text_value: None,
}
}
/// Creates a new `Bar` with the given `label` and value.
///
/// a `label` can be a [`&str`], [`String`] or anything that can be converted into [`Line`].
///
/// # Examples
///
/// ```
/// use ratatui::widgets::Bar;
///
/// let bar = Bar::with_label("Label", 42);
/// ```
pub fn with_label<T: Into<Line<'a>>>(label: T, value: u64) -> Self {
Self {
value,
label: Some(label.into()),
style: Style::new(),
value_style: Style::new(),
text_value: None,
}
}
/// Set the value of this bar.
///
/// The value will be displayed inside the bar.
///
/// # See also
///
/// [`Bar::value_style`] to style the value.
/// [`Bar::text_value`] to set the displayed value.
/// - [`Bar::value_style`] to style the value.
/// - [`Bar::text_value`] to set the displayed value.
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn value(mut self, value: u64) -> Self {
self.value = value;
@@ -57,14 +103,35 @@ impl<'a> Bar<'a> {
/// Set the label of the bar.
///
/// For [`Vertical`](crate::layout::Direction::Vertical) bars,
/// `label` can be a [`&str`], [`String`] or anything that can be converted into [`Line`].
///
/// # Examples
///
/// From [`&str`] and [`String`]:
///
/// ```rust
/// use ratatui::widgets::Bar;
///
/// Bar::default().label("label");
/// Bar::default().label(String::from("label"));
/// ```
///
/// From a [`Line`] with red foreground color:
///
/// ```rust
/// use ratatui::{style::Stylize, text::Line, widgets::Bar};
///
/// Bar::default().label(Line::from("Line").red());
/// ```
///
/// For [`Vertical`](ratatui_core::layout::Direction::Vertical) bars,
/// display the label **under** the bar.
/// For [`Horizontal`](crate::layout::Direction::Horizontal) bars,
/// For [`Horizontal`](ratatui_core::layout::Direction::Horizontal) bars,
/// display the label **in** the bar.
/// See [`BarChart::direction`](crate::widgets::BarChart::direction) to set the direction.
/// See [`BarChart::direction`](crate::barchart::BarChart::direction) to set the direction.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn label(mut self, label: Line<'a>) -> Self {
self.label = Some(label);
pub fn label<T: Into<Line<'a>>>(mut self, label: T) -> Self {
self.label = Some(label.into());
self
}
@@ -74,6 +141,8 @@ impl<'a> Bar<'a> {
/// your own type that implements [`Into<Style>`]).
///
/// This will apply to every non-styled element. It can be seen and used as a default value.
///
/// [`Color`]: ratatui_core::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
@@ -88,6 +157,8 @@ impl<'a> Bar<'a> {
/// # See also
///
/// [`Bar::value`] to set the value.
///
/// [`Color`]: ratatui_core::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn value_style<S: Into<Style>>(mut self, style: S) -> Self {
self.value_style = style.into();
@@ -96,15 +167,28 @@ impl<'a> Bar<'a> {
/// Set the text value printed in the bar.
///
/// `text_value` can be a [`&str`], `Number` or anything that can be converted into [`String`].
///
/// If `text_value` is not set, then the [`ToString`] representation of `value` will be shown on
/// the bar.
///
/// # Examples
///
/// From [`&str`] and [`String`]:
///
/// ```
/// use ratatui::widgets::Bar;
///
/// Bar::default().text_value("label");
/// Bar::default().text_value(String::from("label"));
/// ```
///
/// # See also
///
/// [`Bar::value`] to set the value.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn text_value(mut self, text_value: String) -> Self {
self.text_value = Some(text_value);
pub fn text_value<T: Into<String>>(mut self, text_value: T) -> Self {
self.text_value = Some(text_value.into());
self
}
@@ -200,3 +284,46 @@ impl<'a> Bar<'a> {
}
}
}
impl<'a> Styled for Bar<'a> {
type Item = Self;
fn style(&self) -> Style {
self.style
}
fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
self.style = style.into();
self
}
}
#[cfg(test)]
mod tests {
use ratatui_core::style::{Color, Modifier, Style, Stylize};
use super::*;
#[test]
fn test_bar_new() {
let bar = Bar::new(42).label(Line::from("Label"));
assert_eq!(bar.label, Some(Line::from("Label")));
assert_eq!(bar.value, 42);
}
#[test]
fn test_bar_with_label() {
let bar = Bar::with_label("Label", 42);
assert_eq!(bar.label, Some(Line::from("Label")));
assert_eq!(bar.value, 42);
}
#[test]
fn test_bar_stylized() {
let bar = Bar::default().red().bold();
assert_eq!(
bar.style,
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)
);
}
}

View File

@@ -1,16 +1,21 @@
use super::Bar;
use crate::prelude::*;
use ratatui_core::{
buffer::Buffer,
layout::{Alignment, Rect},
style::Style,
text::Line,
widgets::Widget,
};
use crate::barchart::Bar;
/// A group of bars to be shown by the Barchart.
///
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::{Bar, BarGroup};
///
/// BarGroup::default()
/// .label("Group 1".into())
/// .bars(&[Bar::default().value(200), Bar::default().value(150)]);
/// let group = BarGroup::new([Bar::with_label("Red", 20), Bar::with_label("Blue", 15)]);
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct BarGroup<'a> {
@@ -21,10 +26,50 @@ pub struct BarGroup<'a> {
}
impl<'a> BarGroup<'a> {
/// Creates a new `BarGroup` with the given bars.
///
/// # Examples
///
/// ```
/// use ratatui::{
/// style::{Style, Stylize},
/// widgets::{Bar, BarGroup},
/// };
///
/// let group = BarGroup::new(vec![Bar::with_label("A", 10), Bar::with_label("B", 20)]);
/// ```
pub fn new<T: Into<Vec<Bar<'a>>>>(bars: T) -> Self {
Self {
bars: bars.into(),
..Self::default()
}
}
/// Set the group label
///
/// `label` can be a [`&str`], [`String`] or anything that can be converted into [`Line`].
///
/// # Examples
///
/// From [`&str`] and [`String`].
///
/// ```rust
/// use ratatui::widgets::BarGroup;
///
/// BarGroup::default().label("label");
/// BarGroup::default().label(String::from("label"));
/// ```
///
/// From a [`Line`] with red foreground color:
///
/// ```rust
/// use ratatui::{style::Stylize, text::Line, widgets::BarGroup};
///
/// BarGroup::default().label(Line::from("Line").red());
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn label(mut self, label: Line<'a>) -> Self {
self.label = Some(label);
pub fn label<T: Into<Line<'a>>>(mut self, label: T) -> Self {
self.label = Some(label.into());
self
}
@@ -70,7 +115,7 @@ impl<'a> From<&[(&'a str, u64)]> for BarGroup<'a> {
label: None,
bars: value
.iter()
.map(|&(text, v)| Bar::default().value(v).label(text.into()))
.map(|&(text, v)| Bar::with_label(text, v))
.collect(),
}
}
@@ -89,3 +134,16 @@ impl<'a> From<&Vec<(&'a str, u64)>> for BarGroup<'a> {
Self::from(array)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bargroup_new() {
let group = BarGroup::new([Bar::with_label("Label1", 1), Bar::with_label("Label2", 2)])
.label(Line::from("Group1"));
assert_eq!(group.label, Some(Line::from("Group1")));
assert_eq!(group.bars.len(), 2);
}
}

View File

@@ -6,17 +6,25 @@
//! [title](Block::title) and [padding](Block::padding).
use itertools::Itertools;
use strum::{Display, EnumString};
use ratatui_core::{
buffer::Buffer,
layout::{Alignment, Rect},
style::{Style, Styled},
symbols::border,
text::Line,
widgets::Widget,
};
use crate::{prelude::*, style::Styled, symbols::border, widgets::Borders};
pub use self::{
padding::Padding,
title::{Position, Title},
};
use crate::borders::{BorderType, Borders};
mod padding;
pub mod title;
pub use padding::Padding;
pub use title::{Position, Title};
/// Base widget to be used to display a box border around all [upper level ones](crate::widgets).
/// Base widget to be used to display a box border around all other built-in widgets.
///
/// The borders can be configured with [`Block::borders`] and others. A block can have multiple
/// [`Title`] using [`Block::title`]. It can also be [styled](Block::style) and
@@ -67,7 +75,10 @@ pub use title::{Position, Title};
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Color, Style},
/// widgets::{Block, BorderType, Borders},
/// };
///
/// Block::new()
/// .border_type(BorderType::Rounded)
@@ -79,12 +90,9 @@ pub use title::{Position, Title};
///
/// You may also use multiple titles like in the following:
/// ```
/// use ratatui::{
/// prelude::*,
/// widgets::{
/// block::{Position, Title},
/// Block,
/// },
/// use ratatui::widgets::{
/// block::{Position, Title},
/// Block,
/// };
///
/// Block::new()
@@ -94,10 +102,7 @@ pub use title::{Position, Title};
///
/// You can also pass it as parameters of another widget so that the block surrounds them:
/// ```
/// use ratatui::{
/// prelude::*,
/// widgets::{Block, Borders, List},
/// };
/// use ratatui::widgets::{Block, Borders, List};
///
/// let surrounding_block = Block::default()
/// .borders(Borders::ALL)
@@ -108,7 +113,7 @@ pub use title::{Position, Title};
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Block<'a> {
/// List of titles
titles: Vec<Title<'a>>,
titles: Vec<(Option<Position>, Line<'a>)>,
/// The style to be patched to all titles of the block
titles_style: Style,
/// The default alignment of the titles that don't have one
@@ -128,79 +133,6 @@ pub struct Block<'a> {
padding: Padding,
}
/// The type of border of a [`Block`].
///
/// See the [`borders`](Block::borders) method of `Block` to configure its borders.
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum BorderType {
/// A plain, simple border.
///
/// This is the default
///
/// # Example
///
/// ```plain
/// ┌───────┐
/// │ │
/// └───────┘
/// ```
#[default]
Plain,
/// A plain border with rounded corners.
///
/// # Example
///
/// ```plain
/// ╭───────╮
/// │ │
/// ╰───────╯
/// ```
Rounded,
/// A doubled border.
///
/// Note this uses one character that draws two lines.
///
/// # Example
///
/// ```plain
/// ╔═══════╗
/// ║ ║
/// ╚═══════╝
/// ```
Double,
/// A thick border.
///
/// # Example
///
/// ```plain
/// ┏━━━━━━━┓
/// ┃ ┃
/// ┗━━━━━━━┛
/// ```
Thick,
/// A border with a single line on the inside of a half block.
///
/// # Example
///
/// ```plain
/// ▗▄▄▄▄▄▄▄▖
/// ▐ ▌
/// ▐ ▌
/// ▝▀▀▀▀▀▀▀▘
QuadrantInside,
/// A border with a single line on the outside of a half block.
///
/// # Example
///
/// ```plain
/// ▛▀▀▀▀▀▀▀▜
/// ▌ ▐
/// ▌ ▐
/// ▙▄▄▄▄▄▄▄▟
QuadrantOutside,
}
impl<'a> Block<'a> {
/// Creates a new block with no [`Borders`] or [`Padding`].
pub const fn new() -> Self {
@@ -220,7 +152,8 @@ impl<'a> Block<'a> {
/// Create a new block with [all borders](Borders::ALL) shown
///
/// ```
/// # use ratatui::widgets::{Block, Borders};
/// use ratatui::widgets::{Block, Borders};
///
/// assert_eq!(Block::bordered(), Block::new().borders(Borders::ALL));
/// ```
pub const fn bordered() -> Self {
@@ -239,13 +172,13 @@ impl<'a> Block<'a> {
/// space is calculated based on the full width of the block, rather than the leftover width.
///
/// You can provide any type that can be converted into [`Title`] including: strings, string
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](crate::text::Span), or vectors of
/// [spans](crate::text::Span) (`Vec<Span>`).
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](ratatui_core::text::Span), or
/// vectors of [spans](ratatui_core::text::Span) (`Vec<Span>`).
///
/// By default, the titles will avoid being rendered in the corners of the block but will align
/// against the left or right edge of the block if there is no border on that edge.
/// The following demonstrates this behavior, notice the second title is one character off to
/// the left.
/// against the left or right edge of the block if there is no border on that edge. The
/// following demonstrates this behavior, notice the second title is one character off to the
/// left.
///
/// ```plain
/// ┌With at least a left border───
@@ -268,15 +201,15 @@ impl<'a> Block<'a> {
/// - Two titles with the same alignment (notice the left titles are separated)
/// ```
/// use ratatui::{
/// prelude::*,
/// widgets::{block::*, *},
/// text::Line,
/// widgets::{Block, Borders},
/// };
///
/// Block::new()
/// .title("Title") // By default in the top left corner
/// .title(Title::from("Left").alignment(Alignment::Left)) // also on the left
/// .title(Title::from("Right").alignment(Alignment::Right))
/// .title(Title::from("Center").alignment(Alignment::Center));
/// .title(Line::from("Left").left_aligned()) // also on the left
/// .title(Line::from("Right").right_aligned())
/// .title(Line::from("Center").centered());
/// // Renders
/// // ┌Title─Left────Center─────────Right┐
/// ```
@@ -288,26 +221,40 @@ impl<'a> Block<'a> {
/// - [`Block::title_alignment`]
/// - [`Block::title_position`]
///
/// [Block example]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md#block
/// # Future improvements
///
/// In a future release of Ratatui this method will be changed to accept `Into<Line>` instead of
/// `Into<Title>`. This allows us to remove the unnecessary `Title` struct and store the
/// position in the block itself. For more information see
/// <https://github.com/ratatui/ratatui/issues/738>.
///
/// [Block example]: https://github.com/ratatui/ratatui/blob/main/examples/README.md#block
#[must_use = "method moves the value of self and returns the modified value"]
pub fn title<T>(mut self, title: T) -> Self
where
T: Into<Title<'a>>,
{
self.titles.push(title.into());
let title = title.into();
let position = title.position;
let mut content = title.content;
if let Some(alignment) = title.alignment {
content = content.alignment(alignment);
}
self.titles.push((position, content));
self
}
/// Adds a title to the top of the block.
///
/// You can provide any type that can be converted into [`Line`] including: strings, string
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](crate::text::Span), or vectors of
/// [spans](crate::text::Span) (`Vec<Span>`).
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](ratatui_core::text::Span), or
/// vectors of [spans](ratatui_core::text::Span) (`Vec<Span>`).
///
/// # Example
///
/// ```
/// # use ratatui::{ prelude::*, widgets::* };
/// use ratatui::{ widgets::Block, text::Line };
///
/// Block::bordered()
/// .title_top("Left1") // By default in the top left corner
/// .title_top(Line::from("Left2").left_aligned())
@@ -321,21 +268,22 @@ impl<'a> Block<'a> {
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn title_top<T: Into<Line<'a>>>(mut self, title: T) -> Self {
let title = Title::from(title).position(Position::Top);
self.titles.push(title);
let line = title.into();
self.titles.push((Some(Position::Top), line));
self
}
/// Adds a title to the bottom of the block.
///
/// You can provide any type that can be converted into [`Line`] including: strings, string
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](crate::text::Span), or vectors of
/// [spans](crate::text::Span) (`Vec<Span>`).
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](ratatui_core::text::Span), or
/// vectors of [spans](ratatui_core::text::Span) (`Vec<Span>`).
///
/// # Example
///
/// ```
/// # use ratatui::{ prelude::*, widgets::* };
/// use ratatui::{ widgets::Block, text::Line };
///
/// Block::bordered()
/// .title_bottom("Left1") // By default in the top left corner
/// .title_bottom(Line::from("Left2").left_aligned())
@@ -349,8 +297,8 @@ impl<'a> Block<'a> {
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn title_bottom<T: Into<Line<'a>>>(mut self, title: T) -> Self {
let title = Title::from(title).position(Position::Bottom);
self.titles.push(title);
let line = title.into();
self.titles.push((Some(Position::Bottom), line));
self
}
@@ -364,6 +312,8 @@ impl<'a> Block<'a> {
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// [`Color`]: ratatui_core::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn title_style<S: Into<Style>>(mut self, style: S) -> Self {
self.titles_style = style.into();
@@ -379,15 +329,12 @@ impl<'a> Block<'a> {
/// This example aligns all titles in the center except the "right" title which explicitly sets
/// [`Alignment::Right`].
/// ```
/// use ratatui::{
/// prelude::*,
/// widgets::{block::*, *},
/// };
/// use ratatui::{layout::Alignment, text::Line, widgets::Block};
///
/// Block::new()
/// .title_alignment(Alignment::Center)
/// // This title won't be aligned in the center
/// .title(Title::from("right").alignment(Alignment::Right))
/// .title(Line::from("right").right_aligned())
/// .title("foo")
/// .title("bar");
/// ```
@@ -406,18 +353,12 @@ impl<'a> Block<'a> {
/// This example positions all titles on the bottom except the "top" title which explicitly sets
/// [`Position::Top`].
/// ```
/// use ratatui::{
/// prelude::*,
/// widgets::{
/// block::{Position, Title},
/// Block,
/// },
/// };
/// use ratatui::widgets::{block::Position, Block};
///
/// Block::new()
/// .title_position(Position::Bottom)
/// // This title won't be aligned in the center
/// .title(Title::from("top").position(Position::Top))
/// .title_top("top")
/// .title("foo")
/// .title("bar");
/// ```
@@ -441,9 +382,14 @@ impl<'a> Block<'a> {
///
/// This example shows a `Block` with blue borders.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Style, Stylize},
/// widgets::Block,
/// };
/// Block::bordered().border_style(Style::new().blue());
/// ```
///
/// [`Color`]: ratatui_core::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn border_style<S: Into<Style>>(mut self, style: S) -> Self {
self.border_style = style.into();
@@ -466,7 +412,11 @@ impl<'a> Block<'a> {
/// # Example
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{
/// style::{Color, Style, Stylize},
/// widgets::{Block, Paragraph},
/// };
///
/// let block = Block::new().style(Style::new().red().on_black());
///
/// // For border and title you can additionally apply styles on top of the block level style.
@@ -482,7 +432,8 @@ impl<'a> Block<'a> {
/// .style(Style::new().white().not_bold()); // will be white, and italic
/// ```
///
/// [`Paragraph`]: crate::widgets::Paragraph
/// [`Paragraph`]: crate::paragraph::Paragraph
/// [`Color`]: ratatui_core::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
@@ -497,7 +448,7 @@ impl<'a> Block<'a> {
///
/// Display left and right borders.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::{Block, Borders};
/// Block::new().borders(Borders::LEFT | Borders::RIGHT);
/// ```
///
@@ -518,7 +469,7 @@ impl<'a> Block<'a> {
/// # Examples
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::{Block, BorderType};
/// Block::bordered()
/// .border_type(BorderType::Rounded)
/// .title("Block");
@@ -533,14 +484,15 @@ impl<'a> Block<'a> {
self
}
/// Sets the symbols used to display the border as a [`crate::symbols::border::Set`].
/// Sets the symbols used to display the border as a [`ratatui_core::symbols::border::Set`].
///
/// Setting this overwrites any [`border_type`](Block::border_type) that was set.
///
/// # Examples
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{widgets::Block, symbols};
///
/// Block::bordered().border_set(symbols::border::DOUBLE).title("Block");
/// // Renders
/// // ╔Block╗
@@ -560,7 +512,8 @@ impl<'a> Block<'a> {
///
/// This renders a `Block` with no padding (the default).
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::{Block, Padding};
///
/// Block::bordered().padding(Padding::ZERO);
/// // Renders
/// // ┌───────┐
@@ -571,7 +524,8 @@ impl<'a> Block<'a> {
/// This example shows a `Block` with padding left and right ([`Padding::horizontal`]).
/// Notice the two spaces before and after the content.
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::{Block, Padding};
///
/// Block::bordered().padding(Padding::horizontal(2));
/// // Renders
/// // ┌───────────┐
@@ -590,12 +544,13 @@ impl<'a> Block<'a> {
///
/// Draw a block nested within another block
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// use ratatui::{widgets::Block, Frame};
///
/// # fn render_nested_block(frame: &mut Frame) {
/// let outer_block = Block::bordered().title("Outer");
/// let inner_block = Block::bordered().title("Inner");
///
/// let outer_area = frame.size();
/// let outer_area = frame.area();
/// let inner_area = outer_block.inner(outer_area);
///
/// frame.render_widget(outer_block, outer_area);
@@ -642,37 +597,18 @@ impl<'a> Block<'a> {
fn has_title_at_position(&self, position: Position) -> bool {
self.titles
.iter()
.any(|title| title.position.unwrap_or(self.titles_position) == position)
}
}
impl BorderType {
/// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols.
pub const fn border_symbols(border_type: Self) -> border::Set {
match border_type {
Self::Plain => border::PLAIN,
Self::Rounded => border::ROUNDED,
Self::Double => border::DOUBLE,
Self::Thick => border::THICK,
Self::QuadrantInside => border::QUADRANT_INSIDE,
Self::QuadrantOutside => border::QUADRANT_OUTSIDE,
}
}
/// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols.
pub const fn to_border_set(self) -> border::Set {
Self::border_symbols(self)
.any(|(pos, _)| pos.unwrap_or(self.titles_position) == position)
}
}
impl Widget for Block<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
Widget::render(&self, area, buf);
}
}
impl WidgetRef for Block<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
impl Widget for &Block<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let area = area.intersection(buf.area);
if area.is_empty() {
return;
@@ -787,7 +723,7 @@ impl Block<'_> {
/// Currently (due to the way lines are truncated), the right side of the leftmost title will
/// be cut off if the block is too small to fit all titles. This is not ideal and should be
/// the left side of that leftmost that is cut off. This is due to the line being truncated
/// incorrectly. See <https://github.com/ratatui-org/ratatui/issues/932>
/// incorrectly. See <https://github.com/ratatui/ratatui/issues/932>
#[allow(clippy::similar_names)]
fn render_right_titles(&self, position: Position, area: Rect, buf: &mut Buffer) {
let titles = self.filtered_titles(position, Alignment::Right);
@@ -798,7 +734,7 @@ impl Block<'_> {
if titles_area.is_empty() {
break;
}
let title_width = title.content.width() as u16;
let title_width = title.width() as u16;
let title_area = Rect {
x: titles_area
.right()
@@ -808,7 +744,7 @@ impl Block<'_> {
..titles_area
};
buf.set_style(title_area, self.titles_style);
title.content.render_ref(title_area, buf);
title.render(title_area, buf);
// bump the width of the titles area to the left
titles_area.width = titles_area
@@ -830,7 +766,7 @@ impl Block<'_> {
.collect_vec();
let total_width = titles
.iter()
.map(|title| title.content.width() as u16 + 1) // space between titles
.map(|title| title.width() as u16 + 1) // space between titles
.sum::<u16>()
.saturating_sub(1); // no space for the last title
@@ -843,13 +779,13 @@ impl Block<'_> {
if titles_area.is_empty() {
break;
}
let title_width = title.content.width() as u16;
let title_width = title.width() as u16;
let title_area = Rect {
width: title_width.min(titles_area.width),
..titles_area
};
buf.set_style(title_area, self.titles_style);
title.content.render_ref(title_area, buf);
title.render(title_area, buf);
// bump the titles area to the right and reduce its width
titles_area.x = titles_area.x.saturating_add(title_width + 1);
@@ -866,13 +802,13 @@ impl Block<'_> {
if titles_area.is_empty() {
break;
}
let title_width = title.content.width() as u16;
let title_width = title.width() as u16;
let title_area = Rect {
width: title_width.min(titles_area.width),
..titles_area
};
buf.set_style(title_area, self.titles_style);
title.content.render_ref(title_area, buf);
title.render(title_area, buf);
// bump the titles area to the right and reduce its width
titles_area.x = titles_area.x.saturating_add(title_width + 1);
@@ -885,11 +821,12 @@ impl Block<'_> {
&self,
position: Position,
alignment: Alignment,
) -> impl DoubleEndedIterator<Item = &Title> {
self.titles.iter().filter(move |title| {
title.position.unwrap_or(self.titles_position) == position
&& title.alignment.unwrap_or(self.titles_alignment) == alignment
})
) -> impl DoubleEndedIterator<Item = &Line> {
self.titles
.iter()
.filter(move |(pos, _)| pos.unwrap_or(self.titles_position) == position)
.filter(move |(_, line)| line.alignment.unwrap_or(self.titles_alignment) == alignment)
.map(|(_, line)| line)
}
/// An area that is one line tall and spans the width of the block excluding the borders and
@@ -972,6 +909,7 @@ impl<'a> Styled for Block<'a> {
#[cfg(test)]
mod tests {
use ratatui_core::style::{Color, Modifier, Stylize};
use rstest::rstest;
use strum::ParseError;
@@ -1023,24 +961,17 @@ mod tests {
let area = Rect::new(0, 0, 0, 1);
let expected = Rect::new(0, 1, 0, 0);
let block = Block::new().title(Title::from("Test").alignment(alignment));
let block = Block::new().title(Line::from("Test").alignment(alignment));
assert_eq!(block.inner(area), expected);
}
#[rstest]
#[case::top_top(Borders::TOP, Position::Top, Rect::new(0, 1, 0, 1))]
#[case::top_bot(Borders::BOTTOM, Position::Top, Rect::new(0, 1, 0, 0))]
#[case::bot_top(Borders::TOP, Position::Bottom, Rect::new(0, 1, 0, 0))]
#[case::top_top(Borders::BOTTOM, Position::Bottom, Rect::new(0, 0, 0, 1))]
fn inner_takes_into_account_border_and_title(
#[case] borders: Borders,
#[case] position: Position,
#[case] expected: Rect,
) {
#[case::top_top(Block::new().title_top("Test").borders(Borders::TOP), Rect::new(0, 1, 0, 1))]
#[case::top_bot(Block::new().title_top("Test").borders(Borders::BOTTOM), Rect::new(0, 1, 0, 0))]
#[case::bot_top(Block::new().title_bottom("Test").borders(Borders::TOP), Rect::new(0, 1, 0, 0))]
#[case::bot_bot(Block::new().title_bottom("Test").borders(Borders::BOTTOM), Rect::new(0, 0, 0, 1))]
fn inner_takes_into_account_border_and_title(#[case] block: Block, #[case] expected: Rect) {
let area = Rect::new(0, 0, 0, 2);
let block = Block::new()
.borders(borders)
.title(Title::from("Test").position(position));
assert_eq!(block.inner(area), expected);
}
@@ -1050,32 +981,33 @@ mod tests {
assert!(!block.has_title_at_position(Position::Top));
assert!(!block.has_title_at_position(Position::Bottom));
let block = Block::new().title(Title::from("Test").position(Position::Top));
let block = Block::new().title_top("test");
assert!(block.has_title_at_position(Position::Top));
assert!(!block.has_title_at_position(Position::Bottom));
let block = Block::new().title(Title::from("Test").position(Position::Bottom));
let block = Block::new().title_bottom("test");
assert!(!block.has_title_at_position(Position::Top));
assert!(block.has_title_at_position(Position::Bottom));
#[allow(deprecated)] // until Title is removed
let block = Block::new()
.title(Title::from("Test").position(Position::Top))
.title_position(Position::Bottom);
assert!(block.has_title_at_position(Position::Top));
assert!(!block.has_title_at_position(Position::Bottom));
#[allow(deprecated)] // until Title is removed
let block = Block::new()
.title(Title::from("Test").position(Position::Bottom))
.title_position(Position::Top);
assert!(!block.has_title_at_position(Position::Top));
assert!(block.has_title_at_position(Position::Bottom));
let block = Block::new()
.title(Title::from("Test").position(Position::Top))
.title(Title::from("Test").position(Position::Bottom));
let block = Block::new().title_top("test").title_bottom("test");
assert!(block.has_title_at_position(Position::Top));
assert!(block.has_title_at_position(Position::Bottom));
#[allow(deprecated)] // until Title is removed
let block = Block::new()
.title(Title::from("Test").position(Position::Top))
.title(Title::from("Test"))
@@ -1083,6 +1015,7 @@ mod tests {
assert!(block.has_title_at_position(Position::Top));
assert!(block.has_title_at_position(Position::Bottom));
#[allow(deprecated)] // until Title is removed
let block = Block::new()
.title(Title::from("Test"))
.title(Title::from("Test").position(Position::Bottom))
@@ -1130,16 +1063,10 @@ mod tests {
#[test]
fn vertical_space_takes_into_account_titles() {
let block = Block::new()
.title_position(Position::Top)
.title(Title::from("Test"));
let block = Block::new().title_top("Test");
assert_eq!(block.vertical_space(), (1, 0));
let block = Block::new()
.title_position(Position::Bottom)
.title(Title::from("Test"));
let block = Block::new().title_bottom("Test");
assert_eq!(block.vertical_space(), (0, 1));
}
@@ -1158,10 +1085,7 @@ mod tests {
#[case] pos: Position,
#[case] vertical_space: (u16, u16),
) {
let block = block
.borders(borders)
.title_position(pos)
.title(Title::from("Test"));
let block = block.borders(borders).title_position(pos).title("Test");
assert_eq!(block.vertical_space(), vertical_space);
}
@@ -1310,6 +1234,7 @@ mod tests {
use Alignment::*;
use Position::*;
let mut buffer = Buffer::empty(Rect::new(0, 0, 11, 3));
#[allow(deprecated)] // until Title is removed
Block::bordered()
.title(Title::from("A").position(Top).alignment(Left))
.title(Title::from("B").position(Top).alignment(Center))
@@ -1375,13 +1300,13 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 1));
Block::new()
.title_alignment(block_title_alignment)
.title(Title::from("test").alignment(alignment))
.title(Line::from("test").alignment(alignment))
.render(buffer.area, &mut buffer);
assert_eq!(buffer, Buffer::with_lines([expected]));
}
}
/// This is a regression test for bug <https://github.com/ratatui-org/ratatui/issues/929>
/// This is a regression test for bug <https://github.com/ratatui/ratatui/issues/929>
#[test]
fn render_right_aligned_empty_title() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));

View File

@@ -10,7 +10,7 @@
/// # Example
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
/// use ratatui::widgets::Padding;
///
/// Padding::uniform(1);
/// Padding::horizontal(2);
@@ -19,8 +19,8 @@
/// Padding::symmetric(5, 6);
/// ```
///
/// [`Block`]: crate::widgets::Block
/// [`padding`]: crate::widgets::Block::padding
/// [`Block`]: crate::block::Block
/// [`padding`]: crate::block::Block::padding
/// [CSS padding]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Padding {

View File

@@ -1,14 +1,24 @@
//! This module holds the [`Title`] element and its related configuration types.
//! A title is a piece of [`Block`](crate::widgets::Block) configuration.
//! A title is a piece of [`Block`](crate::block::Block) configuration.
use ratatui_core::{layout::Alignment, text::Line};
use strum::{Display, EnumString};
use crate::{layout::Alignment, text::Line};
/// A [`Block`](crate::widgets::Block) title.
/// A [`Block`](crate::block::Block) title.
///
/// It can be aligned (see [`Alignment`]) and positioned (see [`Position`]).
///
/// # Future Deprecation
///
/// This type is deprecated and will be removed in a future release. The reason for this is that the
/// position of the title should be stored in the block itself, not in the title. The `Line` type
/// has an alignment method that can be used to align the title. For more information see
/// <https://github.com/ratatui/ratatui/issues/738>.
///
/// Use [`Line`] instead, when the position is not defined as part of the title. When a specific
/// position is needed, use [`Block::title_top`](crate::block::Block::title_top) or
/// [`Block::title_bottom`](crate::block::Block::title_bottom) instead.
///
/// # Example
///
/// Title with no style.
@@ -18,16 +28,16 @@ use crate::{layout::Alignment, text::Line};
/// Title::from("Title");
/// ```
///
/// Blue title on a white background (via [`Stylize`](crate::style::Stylize) trait).
/// Blue title on a white background (via [`Stylize`](ratatui_core::style::Stylize) trait).
/// ```
/// use ratatui::{prelude::*, widgets::block::*};
/// use ratatui::{style::Stylize, widgets::block::Title};
///
/// Title::from("Title".blue().on_white());
/// ```
///
/// Title with multiple styles (see [`Line`] and [`Stylize`](crate::style::Stylize)).
/// Title with multiple styles (see [`Line`] and [`Stylize`](ratatui_core::style::Stylize)).
/// ```
/// use ratatui::{prelude::*, widgets::block::*};
/// use ratatui::{style::Stylize, text::Line, widgets::block::Title};
///
/// Title::from(Line::from(vec!["Q".white().underlined(), "uit".gray()]));
/// ```
@@ -35,7 +45,7 @@ use crate::{layout::Alignment, text::Line};
/// Complete example
/// ```
/// use ratatui::{
/// prelude::*,
/// layout::Alignment,
/// widgets::{
/// block::{Position, Title},
/// Block,
@@ -53,19 +63,19 @@ pub struct Title<'a> {
/// Title alignment
///
/// If [`None`], defaults to the alignment defined with
/// [`Block::title_alignment`](crate::widgets::Block::title_alignment) in the associated
/// [`Block`](crate::widgets::Block).
/// [`Block::title_alignment`](crate::block::Block::title_alignment) in the associated
/// [`Block`](crate::block::Block).
pub alignment: Option<Alignment>,
/// Title position
///
/// If [`None`], defaults to the position defined with
/// [`Block::title_position`](crate::widgets::Block::title_position) in the associated
/// [`Block`](crate::widgets::Block).
/// [`Block::title_position`](crate::block::Block::title_position) in the associated
/// [`Block`](crate::block::Block).
pub position: Option<Position>,
}
/// Defines the [title](crate::widgets::block::Title) position.
/// Defines the [title](crate::block::Title) position.
///
/// The title can be positioned on top or at the bottom of the block.
/// Defaults to [`Position::Top`].
@@ -73,7 +83,10 @@ pub struct Title<'a> {
/// # Example
///
/// ```
/// use ratatui::widgets::{block::*, *};
/// use ratatui::widgets::{
/// block::{Position, Title},
/// Block,
/// };
///
/// Block::new().title(Title::from("title").position(Position::Bottom));
/// ```
@@ -88,6 +101,7 @@ pub enum Position {
Bottom,
}
#[deprecated = "use Block::title_top() or Block::title_bottom() instead. This will be removed in a future release."]
impl<'a> Title<'a> {
/// Set the title content.
#[must_use = "method moves the value of self and returns the modified value"]

View File

@@ -1,6 +1,9 @@
//! Border related types ([`Borders`], [`BorderType`]) and a macro to create borders ([`border`]).
use std::fmt;
use bitflags::bitflags;
use ratatui_core::symbols::border;
use strum::{Display, EnumString};
bitflags! {
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
@@ -21,6 +24,98 @@ bitflags! {
}
}
/// The type of border of a [`Block`](crate::block::Block).
///
/// See the [`borders`](crate::block::Block::borders) method of `Block` to configure its borders.
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum BorderType {
/// A plain, simple border.
///
/// This is the default
///
/// # Example
///
/// ```plain
/// ┌───────┐
/// │ │
/// └───────┘
/// ```
#[default]
Plain,
/// A plain border with rounded corners.
///
/// # Example
///
/// ```plain
/// ╭───────╮
/// │ │
/// ╰───────╯
/// ```
Rounded,
/// A doubled border.
///
/// Note this uses one character that draws two lines.
///
/// # Example
///
/// ```plain
/// ╔═══════╗
/// ║ ║
/// ╚═══════╝
/// ```
Double,
/// A thick border.
///
/// # Example
///
/// ```plain
/// ┏━━━━━━━┓
/// ┃ ┃
/// ┗━━━━━━━┛
/// ```
Thick,
/// A border with a single line on the inside of a half block.
///
/// # Example
///
/// ```plain
/// ▗▄▄▄▄▄▄▄▖
/// ▐ ▌
/// ▐ ▌
/// ▝▀▀▀▀▀▀▀▘
QuadrantInside,
/// A border with a single line on the outside of a half block.
///
/// # Example
///
/// ```plain
/// ▛▀▀▀▀▀▀▀▜
/// ▌ ▐
/// ▌ ▐
/// ▙▄▄▄▄▄▄▄▟
QuadrantOutside,
}
impl BorderType {
/// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols.
pub const fn border_symbols(border_type: Self) -> border::Set {
match border_type {
Self::Plain => border::PLAIN,
Self::Rounded => border::ROUNDED,
Self::Double => border::DOUBLE,
Self::Thick => border::THICK,
Self::QuadrantInside => border::QUADRANT_INSIDE,
Self::QuadrantOutside => border::QUADRANT_OUTSIDE,
}
}
/// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols.
pub const fn to_border_set(self) -> border::Set {
Self::border_symbols(self)
}
}
/// Implement the `Debug` trait for the `Borders` bitflags. This is a manual implementation to
/// display the flags in a more readable way. The default implementation would display the
/// flags as 'Border(0x0)' for `Borders::NONE` for example.
@@ -55,12 +150,16 @@ impl fmt::Debug for Borders {
/// and RIGHT.
///
/// When used with NONE you should consider omitting this completely. For ALL you should consider
/// [`Block::bordered()`](crate::widgets::Block::bordered) instead.
/// [`Block::bordered()`](crate::block::Block::bordered) instead.
///
/// ## Examples
///
/// ```
/// # use ratatui::{border, prelude::*, widgets::*};
/// use ratatui::{
/// border,
/// widgets::{Block, Borders},
/// };
///
/// Block::new()
/// .title("Construct Borders and use them in place")
/// .borders(border!(TOP, BOTTOM));
@@ -69,7 +168,7 @@ impl fmt::Debug for Borders {
/// `border!` can be called with any number of individual sides:
///
/// ```
/// # use ratatui::{border, prelude::*, widgets::*};
/// use ratatui::{border, widgets::Borders};
/// let right_open = border!(TOP, LEFT, BOTTOM);
/// assert_eq!(right_open, Borders::TOP | Borders::LEFT | Borders::BOTTOM);
/// ```
@@ -77,12 +176,12 @@ impl fmt::Debug for Borders {
/// Single borders work but using `Borders::` directly would be simpler.
///
/// ```
/// # use ratatui::{border, prelude::*, widgets::*};
/// use ratatui::{border, widgets::Borders};
///
/// assert_eq!(border!(TOP), Borders::TOP);
/// assert_eq!(border!(ALL), Borders::ALL);
/// assert_eq!(border!(), Borders::NONE);
/// ```
#[cfg(feature = "macros")]
#[macro_export]
macro_rules! border {
() => {
@@ -119,11 +218,6 @@ mod tests {
"TOP | BOTTOM"
);
}
}
#[cfg(all(test, feature = "macros"))]
mod macro_tests {
use super::*;
#[test]
fn can_be_const() {

View File

@@ -10,9 +10,16 @@
//! [`Monthly`] has several controls for what should be displayed
use std::collections::HashMap;
use ratatui_core::{
buffer::Buffer,
layout::{Alignment, Constraint, Layout, Rect},
style::Style,
text::{Line, Span},
widgets::Widget,
};
use time::{Date, Duration, OffsetDateTime};
use crate::{prelude::*, widgets::Block};
use crate::block::{Block, BlockExt};
/// Display a month calendar for the month containing `display_date`
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
@@ -46,6 +53,8 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// [`Color`]: ratatui_core::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn show_surrounding<S: Into<Style>>(mut self, style: S) -> Self {
self.show_surrounding = Some(style.into());
@@ -56,6 +65,8 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// [`Color`]: ratatui_core::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn show_weekdays_header<S: Into<Style>>(mut self, style: S) -> Self {
self.show_weekday = Some(style.into());
@@ -66,6 +77,8 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// [`Color`]: ratatui_core::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn show_month_header<S: Into<Style>>(mut self, style: S) -> Self {
self.show_month = Some(style.into());
@@ -76,6 +89,8 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// [`Color`]: ratatui_core::style::Color
#[must_use = "method moves the value of self and returns the modified value"]
pub fn default_style<S: Into<Style>>(mut self, style: S) -> Self {
self.default_style = style.into();
@@ -121,13 +136,13 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
impl<DS: DateStyler> Widget for Monthly<'_, DS> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
Widget::render(&self, area, buf);
}
}
impl<DS: DateStyler> WidgetRef for Monthly<'_, DS> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
self.block.render_ref(area, buf);
impl<DS: DateStyler> Widget for &Monthly<'_, DS> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.block.as_ref().render(area, buf);
let inner = self.block.inner_if_some(area);
self.render_monthly(inner, buf);
}
@@ -177,7 +192,9 @@ impl<DS: DateStyler> Monthly<'_, DS> {
spans.push(self.format_date(curr_day));
curr_day += Duration::DAY;
}
buf.set_line(days_area.x, y, &spans.into(), area.width);
if buf.area.height > y {
buf.set_line(days_area.x, y, &spans.into(), area.width);
}
y += 1;
}
}
@@ -199,6 +216,8 @@ impl CalendarEventStore {
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// [`Color`]: ratatui_core::style::Color
pub fn today<S: Into<Style>>(style: S) -> Self {
let mut res = Self::default();
res.add(
@@ -214,6 +233,8 @@ impl CalendarEventStore {
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// [`Color`]: ratatui_core::style::Color
pub fn add<S: Into<Style>>(&mut self, date: Date, style: S) {
// to simplify style nonsense, last write wins
let _ = self.0.insert(date, style.into());
@@ -245,6 +266,7 @@ impl Default for CalendarEventStore {
#[cfg(test)]
mod tests {
use ratatui_core::style::Color;
use time::Month;
use super::*;

View File

@@ -12,16 +12,18 @@
//! - [`Rectangle`]: A basic rectangle
//!
//! You can also implement your own custom [`Shape`]s.
mod circle;
mod line;
mod map;
mod points;
mod rectangle;
mod world;
use std::{fmt, iter::zip};
use itertools::Itertools;
use ratatui_core::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
symbols::{self, Marker},
text::Line as TextLine,
widgets::Widget,
};
pub use self::{
circle::Circle,
@@ -30,7 +32,14 @@ pub use self::{
points::Points,
rectangle::Rectangle,
};
use crate::{prelude::*, symbols::Marker, text::Line as TextLine, widgets::Block};
use crate::block::{Block, BlockExt};
mod circle;
mod line;
mod map;
mod points;
mod rectangle;
mod world;
/// Something that can be drawn on a [`Canvas`].
///
@@ -353,10 +362,16 @@ impl<'a, 'b> Painter<'a, 'b> {
/// and `[0, height - 1]` respectively. The resolution of the grid is used to convert the
/// `(x, y)` coordinates to the location of a point on the grid.
///
/// Points are rounded to the nearest grid cell (with points exactly in the center of a cell
/// rounding up).
///
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::canvas::*};
/// use ratatui::{
/// symbols,
/// widgets::canvas::{Context, Painter},
/// };
///
/// let mut ctx = Context::new(2, 2, [1.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
/// let mut painter = Painter::from(&mut ctx);
@@ -365,7 +380,7 @@ impl<'a, 'b> Painter<'a, 'b> {
/// assert_eq!(point, Some((0, 7)));
///
/// let point = painter.get_point(1.5, 1.0);
/// assert_eq!(point, Some((1, 3)));
/// assert_eq!(point, Some((2, 4)));
///
/// let point = painter.get_point(0.0, 0.0);
/// assert_eq!(point, None);
@@ -377,20 +392,18 @@ impl<'a, 'b> Painter<'a, 'b> {
/// assert_eq!(point, Some((0, 0)));
/// ```
pub fn get_point(&self, x: f64, y: f64) -> Option<(usize, usize)> {
let left = self.context.x_bounds[0];
let right = self.context.x_bounds[1];
let top = self.context.y_bounds[1];
let bottom = self.context.y_bounds[0];
let [left, right] = self.context.x_bounds;
let [bottom, top] = self.context.y_bounds;
if x < left || x > right || y < bottom || y > top {
return None;
}
let width = (self.context.x_bounds[1] - self.context.x_bounds[0]).abs();
let height = (self.context.y_bounds[1] - self.context.y_bounds[0]).abs();
if width == 0.0 || height == 0.0 {
let width = right - left;
let height = top - bottom;
if width <= 0.0 || height <= 0.0 {
return None;
}
let x = ((x - left) * (self.resolution.0 - 1.0) / width) as usize;
let y = ((top - y) * (self.resolution.1 - 1.0) / height) as usize;
let x = ((x - left) * (self.resolution.0 - 1.0) / width).round() as usize;
let y = ((top - y) * (self.resolution.1 - 1.0) / height).round() as usize;
Some((x, y))
}
@@ -399,7 +412,11 @@ impl<'a, 'b> Painter<'a, 'b> {
/// # Example
///
/// ```
/// use ratatui::{prelude::*, widgets::canvas::*};
/// use ratatui::{
/// style::Color,
/// symbols,
/// widgets::canvas::{Context, Painter},
/// };
///
/// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
/// let mut painter = Painter::from(&mut ctx);
@@ -408,6 +425,25 @@ impl<'a, 'b> Painter<'a, 'b> {
pub fn paint(&mut self, x: usize, y: usize, color: Color) {
self.context.grid.paint(x, y, color);
}
/// Canvas context bounds by axis.
///
/// # Example
///
/// ```
/// use ratatui::{
/// style::Color,
/// symbols,
/// widgets::canvas::{Context, Painter},
/// };
///
/// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
/// let mut painter = Painter::from(&mut ctx);
/// assert_eq!(painter.bounds(), (&[0.0, 2.0], &[0.0, 2.0]));
/// ```
pub fn bounds(&self) -> (&[f64; 2], &[f64; 2]) {
(&self.context.x_bounds, &self.context.y_bounds)
}
}
impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {
@@ -423,9 +459,7 @@ impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {
/// Holds the state of the [`Canvas`] when painting to it.
///
/// This is used by the [`Canvas`] widget to draw shapes on the grid. It can be useful to think of
/// this as similar to the [`Frame`] struct that is used to draw widgets on the terminal.
///
/// [`Frame`]: crate::prelude::Frame
/// this as similar to the `Frame` struct that is used to draw widgets on the terminal.
#[derive(Debug)]
pub struct Context<'a> {
x_bounds: [f64; 2],
@@ -449,7 +483,7 @@ impl<'a> Context<'a> {
/// example, if you want to draw a map of the world, you might want to use the following bounds:
///
/// ```
/// use ratatui::{prelude::*, widgets::canvas::*};
/// use ratatui::{symbols, widgets::canvas::Context};
///
/// let ctx = Context::new(
/// 100,
@@ -513,6 +547,8 @@ impl<'a> Context<'a> {
///
/// Note that the text is always printed on top of the canvas and is **not** affected by the
/// layers.
///
/// [`Text`]: ratatui_core::text::Text
pub fn print<T>(&mut self, x: f64, y: f64, line: T)
where
T: Into<TextLine<'a>>,
@@ -563,7 +599,10 @@ impl<'a> Context<'a> {
/// ```
/// use ratatui::{
/// style::Color,
/// widgets::{canvas::*, *},
/// widgets::{
/// canvas::{Canvas, Line, Map, MapResolution, Rectangle},
/// Block,
/// },
/// };
///
/// Canvas::default()
@@ -690,15 +729,15 @@ where
/// cell. This allows for more flexibility than the `BrailleGrid` which only supports a single
/// foreground color for each 2x4 dots cell.
///
/// [`Braille`]: crate::symbols::Marker::Braille
/// [`HalfBlock`]: crate::symbols::Marker::HalfBlock
/// [`Dot`]: crate::symbols::Marker::Dot
/// [`Block`]: crate::symbols::Marker::Block
/// [`Braille`]: ratatui_core::symbols::Marker::Braille
/// [`HalfBlock`]: ratatui_core::symbols::Marker::HalfBlock
/// [`Dot`]: ratatui_core::symbols::Marker::Dot
/// [`Block`]: ratatui_core::symbols::Marker::Block
///
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::canvas::*};
/// use ratatui::{symbols, widgets::canvas::Canvas};
///
/// Canvas::default()
/// .marker(symbols::Marker::Braille)
@@ -728,16 +767,16 @@ where
F: Fn(&mut Context),
{
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
Widget::render(&self, area, buf);
}
}
impl<F> WidgetRef for Canvas<'_, F>
impl<F> Widget for &Canvas<'_, F>
where
F: Fn(&mut Context),
{
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
self.block.render_ref(area, buf);
fn render(self, area: Rect, buf: &mut Buffer) {
self.block.as_ref().render(area, buf);
let canvas_area = self.block.inner_if_some(area);
if canvas_area.is_empty() {
return;
@@ -809,9 +848,9 @@ where
#[cfg(test)]
mod tests {
use indoc::indoc;
use ratatui_core::buffer::Cell;
use super::*;
use crate::buffer::Cell;
// helper to test the canvas checks that drawing a vertical and horizontal line
// results in the expected output

View File

@@ -1,7 +1,6 @@
use crate::{
style::Color,
widgets::canvas::{Painter, Shape},
};
use ratatui_core::style::Color;
use crate::canvas::{Painter, Shape};
/// A circle with a given center and radius and with a given color
#[derive(Debug, Default, Clone, PartialEq)]
@@ -31,17 +30,12 @@ impl Shape for Circle {
#[cfg(test)]
mod tests {
use crate::{
buffer::Buffer,
layout::Rect,
style::Color,
symbols::Marker,
widgets::{
canvas::{Canvas, Circle},
Widget,
},
use ratatui_core::{
buffer::Buffer, layout::Rect, style::Color, symbols::Marker, widgets::Widget,
};
use crate::canvas::{Canvas, Circle};
#[test]
fn test_it_draws_a_circle() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));
@@ -59,10 +53,10 @@ mod tests {
.y_bounds([-10.0, 10.0]);
canvas.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines([
" ⢀⣠⢤",
" ⢰⠋ ",
" ⠘⣆⡀ ⣠⠇",
" ⠉⠉ ",
" ⣀⣀",
" ⡞⠁",
" ⢇⡀ ⢀⡼",
" ⠉⠉ ",
" ",
]);
assert_eq!(buffer, expected);

View File

@@ -1,7 +1,7 @@
use crate::{
style::Color,
widgets::canvas::{Painter, Shape},
};
use line_clipping::{cohen_sutherland, LineSegment, Point, Window};
use ratatui_core::style::Color;
use crate::canvas::{Painter, Shape};
/// A line from `(x1, y1)` to `(x2, y2)` with the given color
#[derive(Debug, Default, Clone, PartialEq)]
@@ -32,13 +32,21 @@ impl Line {
}
impl Shape for Line {
#[allow(clippy::similar_names)]
fn draw(&self, painter: &mut Painter) {
let Some((x1, y1)) = painter.get_point(self.x1, self.y1) else {
let (x_bounds, y_bounds) = painter.bounds();
let Some((world_x1, world_y1, world_x2, world_y2)) =
clip_line(x_bounds, y_bounds, self.x1, self.y1, self.x2, self.y2)
else {
return;
};
let Some((x2, y2)) = painter.get_point(self.x2, self.y2) else {
let Some((x1, y1)) = painter.get_point(world_x1, world_y1) else {
return;
};
let Some((x2, y2)) = painter.get_point(world_x2, world_y2) else {
return;
};
let (dx, x_range) = if x2 >= x1 {
(x2 - x1, x1..=x2)
} else {
@@ -72,6 +80,27 @@ impl Shape for Line {
}
}
fn clip_line(
&[xmin, xmax]: &[f64; 2],
&[ymin, ymax]: &[f64; 2],
x1: f64,
y1: f64,
x2: f64,
y2: f64,
) -> Option<(f64, f64, f64, f64)> {
if let Some(LineSegment {
p1: Point { x: x1, y: y1 },
p2: Point { x: x2, y: y2 },
}) = cohen_sutherland::clip_line(
LineSegment::new(Point::new(x1, y1), Point::new(x2, y2)),
Window::new(xmin, xmax, ymin, ymax),
) {
Some((x1, y1, x2, y2))
} else {
None
}
}
fn draw_line_low(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: usize, color: Color) {
let dx = (x2 - x1) as isize;
let dy = (y2 as isize - y1 as isize).abs();
@@ -112,21 +141,24 @@ fn draw_line_high(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: us
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
use crate::{
use ratatui_core::{
buffer::Buffer,
layout::Rect,
style::{Style, Stylize},
symbols::Marker,
widgets::{canvas::Canvas, Widget},
widgets::Widget,
};
use rstest::rstest;
use super::*;
use crate::canvas::Canvas;
#[rstest]
#[case::off_grid(&Line::new(-1.0, -1.0, 10.0, 10.0, Color::Red), [" "; 10])]
#[case::off_grid(&Line::new(0.0, 0.0, 11.0, 11.0, Color::Red), [" "; 10])]
#[case::horizontal(&Line::new(0.0, 0.0, 10.0, 0.0, Color::Red), [
#[case::off_grid1(&Line::new(-1.0, 0.0, -1.0, 10.0, Color::Red), [" "; 10])]
#[case::off_grid2(&Line::new(0.0, -1.0, 10.0, -1.0, Color::Red), [" "; 10])]
#[case::off_grid3(&Line::new(-10.0, 5.0, -1.0, 5.0, Color::Red), [" "; 10])]
#[case::off_grid4(&Line::new(5.0, 11.0, 5.0, 20.0, Color::Red), [" "; 10])]
#[case::off_grid5(&Line::new(-10.0, 0.0, 5.0, 0.0, Color::Red), [
" ",
" ",
" ",
@@ -136,50 +168,99 @@ mod tests {
" ",
" ",
" ",
"••••••••••",
"•••••• ",
])]
#[case::horizontal(&Line::new(10.0, 10.0, 0.0, 10.0, Color::Red), [
"••••••••••",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
])]
#[case::vertical(&Line::new(0.0, 0.0, 0.0, 10.0, Color::Red), [""; 10])]
#[case::vertical(&Line::new(10.0, 10.0, 10.0, 0.0, Color::Red), [""; 10])]
// dy < dx, x1 < x2
#[case::diagonal(&Line::new(0.0, 0.0, 10.0, 5.0, Color::Red), [
" ",
" ",
" ",
" ",
#[case::off_grid6(&Line::new(-1.0, -1.0, 10.0, 10.0, Color::Red), [
"",
" ",
" ",
" •• ",
" •• ",
" ",
" ",
" ",
" ",
"",
"",
"",
"",
"",
])]
#[case::off_grid7(&Line::new(0.0, 0.0, 11.0, 11.0, Color::Red), [
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
])]
#[case::off_grid8(&Line::new(-1.0, -1.0, 11.0, 11.0, Color::Red), [
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
])]
#[case::horizontal1(&Line::new(0.0, 0.0, 10.0, 0.0, Color::Red), [
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
"••••••••••",
])]
#[case::horizontal2(&Line::new(10.0, 10.0, 0.0, 10.0, Color::Red), [
"••••••••••",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
])]
#[case::vertical1(&Line::new(0.0, 0.0, 0.0, 10.0, Color::Red), [""; 10])]
#[case::vertical2(&Line::new(10.0, 10.0, 10.0, 0.0, Color::Red), [""; 10])]
// dy < dx, x1 < x2
#[case::diagonal1(&Line::new(0.0, 0.0, 10.0, 5.0, Color::Red), [
" ",
" ",
" ",
" ",
" ",
" ••",
" •• ",
" •• ",
" •• ",
"•• ",
])]
// dy < dx, x1 > x2
#[case::diagonal(&Line::new(10.0, 0.0, 0.0, 5.0, Color::Red), [
#[case::diagonal2(&Line::new(10.0, 0.0, 0.0, 5.0, Color::Red), [
" ",
" ",
" ",
" ",
" ",
" •• ",
" •• ",
" •• ",
" •• ",
" ",
" ",
"•• ",
" •• ",
" •• ",
" •• ",
" ",
])]
// dy > dx, y1 < y2
#[case::diagonal(&Line::new(0.0, 0.0, 5.0, 10.0, Color::Red), [
#[case::diagonal3(&Line::new(0.0, 0.0, 5.0, 10.0, Color::Red), [
"",
"",
"",
"",
@@ -189,11 +270,9 @@ mod tests {
"",
"",
"",
"",
])]
// dy > dx, y1 > y2
#[case::diagonal(&Line::new(0.0, 10.0, 5.0, 0.0, Color::Red), [
"",
#[case::diagonal4(&Line::new(0.0, 10.0, 5.0, 0.0, Color::Red), [
"",
"",
"",
@@ -203,11 +282,12 @@ mod tests {
"",
"",
"",
"",
])]
fn tests<'expected_line, ExpectedLines>(#[case] line: &Line, #[case] expected: ExpectedLines)
where
ExpectedLines: IntoIterator,
ExpectedLines::Item: Into<crate::text::Line<'expected_line>>,
ExpectedLines::Item: Into<ratatui_core::text::Line<'expected_line>>,
{
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
let canvas = Canvas::default()

View File

@@ -0,0 +1,206 @@
use ratatui_core::style::Color;
use strum::{Display, EnumString};
use crate::canvas::{
world::{WORLD_HIGH_RESOLUTION, WORLD_LOW_RESOLUTION},
Painter, Shape,
};
/// Defines how many points are going to be used to draw a [`Map`].
///
/// You generally want a [high](MapResolution::High) resolution map.
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum MapResolution {
/// A lesser resolution for the [`Map`] [`Shape`].
///
/// Contains about 1000 points.
#[default]
Low,
/// A higher resolution for the [`Map`] [`Shape`].
///
/// Contains about 5000 points, you likely want to use [`Marker::Braille`] with this.
///
/// [`Marker::Braille`]: (ratatui_core::symbols::Marker::Braille)
High,
}
impl MapResolution {
const fn data(self) -> &'static [(f64, f64)] {
match self {
Self::Low => &WORLD_LOW_RESOLUTION,
Self::High => &WORLD_HIGH_RESOLUTION,
}
}
}
/// A world map
///
/// A world map can be rendered with different [resolutions](MapResolution) and [colors](Color).
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Map {
/// The resolution of the map.
///
/// This is the number of points used to draw the map.
pub resolution: MapResolution,
/// Map color
///
/// This is the color of the points of the map.
pub color: Color,
}
impl Shape for Map {
fn draw(&self, painter: &mut Painter) {
for (x, y) in self.resolution.data() {
if let Some((x, y)) = painter.get_point(*x, *y) {
painter.paint(x, y, self.color);
}
}
}
}
#[cfg(test)]
mod tests {
use ratatui_core::{buffer::Buffer, layout::Rect, symbols::Marker, widgets::Widget};
use strum::ParseError;
use super::*;
use crate::canvas::Canvas;
#[test]
fn map_resolution_to_string() {
assert_eq!(MapResolution::Low.to_string(), "Low");
assert_eq!(MapResolution::High.to_string(), "High");
}
#[test]
fn map_resolution_from_str() {
assert_eq!("Low".parse(), Ok(MapResolution::Low));
assert_eq!("High".parse(), Ok(MapResolution::High));
assert_eq!(
"".parse::<MapResolution>(),
Err(ParseError::VariantNotFound)
);
}
#[test]
fn default() {
let map = Map::default();
assert_eq!(map.resolution, MapResolution::Low);
assert_eq!(map.color, Color::Reset);
}
#[test]
fn draw_low() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 80, 40));
let canvas = Canvas::default()
.marker(Marker::Dot)
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0])
.paint(|context| {
context.draw(&Map::default());
});
canvas.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines([
" ",
"",
" • •• •••••••• •• •••• ••••• ••• •• ••• ",
" ••••••••••••••• • •••• • • ••••••• ••• ",
" • •••• ••••••••••••••• •• •• • ••• •• •••• •• ••••••• ••• ",
"••••• •••••••••••• •••• • •••••• •••• • ••• ••••• •",
" •• • • •••• •••••••• •••• •• • •• • ••• •• •••",
" •••• ••• •••••• ••••• • •• •••••• • ••••• ",
"••••• ••• • •• •• ••••••• •• •• •• ",
" •• •••• ••••• •• • • • •• ",
" • • ••••••• •• •••• ••• •• • •• • •• ",
" • •• ••••••••• • •• •••• • ",
" •• •• • • • •• • ••••• ",
" •• ••• • •••• • • • ",
" • • •• • •• •• • • ",
" •• • ••••••• • • • • • •• • ",
" ••••••••• • •• • • • •• • •• ",
" •• •• • ••• • ••• •• ",
" ••• • • • • • •• ••• ••• ",
" • • •• • •• ",
" • • ••• • • ••• ••• ",
" • • • • • ••• ",
" • • • • • • • ",
" • • • • ••• •• •",
" • • • • •• • • • ",
" • • • • • ",
" • • • • • ",
" •• •• •• •• • • ",
" • • ••• •• ",
" • • •• •• ",
" • • ",
" ••••• ",
" ",
" •• ",
" ••• • • ••••• • •••• • • •• •• •• ",
" • • • •••••• ••••••••• • •• •• ••• ",
"• ••• •••• •••• • • • ••• • • ••• •",
" •• • • •• • •• •• ",
"• • • ",
" ",
]);
assert_eq!(buffer, expected);
}
#[test]
fn draw_high() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 80, 40));
let canvas = Canvas::default()
.marker(Marker::Braille)
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0])
.paint(|context| {
context.draw(&Map {
resolution: MapResolution::High,
..Default::default()
});
});
canvas.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines([
" ",
" ⢀⣀⣤⠄⠤⠤⣤⣀⡀⣀⣀⡄⠄⢄⣀⣄⡄⢀⡀ ",
" ⢀⣀⣤⠰⢤⣼⡯⢽⡟⣀⢶⣺⡛⠁ ⠈⢰⠃⠁ ⢖⣒⣾⡟⠂ ⠈⠛⠁ ⠺⢩⢖⡄ ",
" ⡬⢍⣿⣟⣿⣻⣿⣿⣿⡾⣯⡀⠈⠁⠁⢦ ⢀⡿ ⠈ ⢠⢶⠘⠋⡁⣀⢠⠤⠖⠘⠉⠁⠈⠼⡧⡄⣄⡀ ⢫⣗⠒⠆ ",
"⣓ ⣠⠖⠓⠒⠢⠤⢄⠤⠶⠽⠽⣶⣃⣽⡮⣿⡷⣗⣤⡭⣍⢓⡄ ⠸⣷ ⢀⣀⠿⠇ ⢀⠔⠒⠲⠄⢄⢀⡀⢙⣑⡄⠴⡍⣟⠉ ⠑⠉⠉ ⠑⠐⠦⠤⣤⠤⢞",
"⠶⢧⣗⢾⡆ ⠈⠈⠁⠈⠉⢀⣹⣶⣩⣽⣐⢮⠃ ⣇ ⢀⡔⠊ ⢰⣖⣲ ⢀⡐⠁⣰⠦ ⢲⣶⠛⠋ ⠐⠋ ⡤",
" ⠉⣮⣀⣀⣴⡤⣠⡀ ⡎ ⠛⢫⠙⢫⢫ ⠈⠦⠼ ⡃⡀⢸⠼⣤⡄ ⡀⣀⣀⡐⡶⣣⢤⠖⠉",
" ⢀⡽⠟⠃ ⠈⠱⡀ ⠙⠢⣀⣨⠆⠈⠁⢧⡀ ⣸⣷ ⢹⣷⣼⣸⠃ ⢀⡐⢀ ⠁⡚⣨⠆ ",
" ⠘⢳⡀ ⠈⠾ ⣀⣀⣽ ⠸⢼⣇⡧⠋⠉⠁ ⠉⣿ ⠢⠂ ",
" ⠈⢻ ⠜⢹⣵⠻⠇ ⠈⢻ ⢀⡀ ⢠⣠⡤ ⢀⢤ ⢰⣯ ",
" ⢼ ⢀⣾⠛⠉ ⠐⡖⠒⡰⢺⣞⣵⡄⢀⣏⡭⣙⡄⢕⢫⡀ ⢀ ⢠⠖⢱⡿⠃ ",
" ⠸ ⠠⡎ ⠰⣅⣰⣃⣘⡣⡿⢻⡿⣁⣀ ⠸⣽ ⠐⣿⣽⣫ ⡸⡇ ",
" ⠳⣄ ⡰⠃ ⢀⠎⠉ ⢧⡀⣠⣛⠈⢻ ⢻⠘⢺⡿⠚⠁ ",
" ⢻⣇ ⣠⠲⠖⢲⡇ ⡸ ⠉⠃⠈⠉⣿ ⢰⣆ ⢸ ⠈⠁ ",
" ⠈⢿⣆ ⡟ ⣘⣻ ⡸ ⢸⢇ ⠈⠯⢿⡒⠲⡀ ⢀⡀ ⣀⢾ ",
" ⠈⢳ ⠸⡀⢳⣠⢾⠉⢹⣦⣤⣀ ⡇ ⡿⡄ ⢰⠃ ⠑⡂ ⢠⠏⢣ ⣼⡮⠁⢈⡀ ",
" ⠙⠲⢆⡿⢦⠈⠉⠁⠁ ⡇ ⠱⣇⣀⠼⠃ ⡃⢰⠃ ⠸⢶ ⠘⠄ ⢾⡁ ",
" ⠙⣾⣀⡴⡶⢤⣤ ⢳ ⠻⠵⡆ ⠸⣸ ⢸⡳⡤⠃⢀⡾⣿ ",
" ⠘⢻⠁ ⠈⠦⣄ ⢧⣀⣀⠤⣀ ⢐⠁ ⠈⠩⠆ ⣘⣧⠁ ⡸⡔⢿ ",
" ⡸ ⢨ ⠁ ⠉⡇ ⢀⠎ ⢻⢿⠄⡴⢑⣧⡠⡄ ",
" ⡇ ⠈⠋⠦⡄ ⠈⡆ ⢠⠃ ⢏⡇⢧⣼⣾⣧⣽⣿⠶⢤⡀⣤ ",
" ⣇ ⠈⡇ ⢸ ⢸ ⠈⠶⣦⣄⣋⣁⡀⠸⣵⢠⣻⠋⠷⣄ ",
" ⠰⡀ ⣰⠁ ⢘⠆ ⢸ ⢠⡀ ⠙⠋⢠⠦⡄⣷⠙⠃ ⠙ ",
"⠄ ⠣⡀ ⡃ ⢸ ⣸⢡⢾⠆ ⡞⠛⠘⢧⡏⡆ ⠸⠄ ⡤",
" ⠱ ⢠⠃ ⠸⡀ ⢸⠁⢸⢨ ⡤⠚ ⠱⡀ ⢦ ⠁",
" ⠅ ⡖⠉ ⡇ ⡜ ⠸⠔ ⡇ ⢳ ",
" ⡇ ⢀⠃ ⢱⡀ ⢰⠃ ⣇ ⢀⡀ ⢸ ",
" ⢀⠃ ⡦⠏ ⠈⠷⠖⠃ ⠾⠴⠊⠁⠹⣦ ⡞ ⣄ ",
" ⢸ ⡤⠃ ⠘⢲⠖⠃ ⣽⡆",
" ⢸ ⣸⠁ ⠈⠿ ⢀⢼⠏ ",
" ⠞ ⡗ ⣄ ⠈⠋ ",
" ⢧⡼⡁⠲⠂ ",
" ⠙⠉ ",
"",
" ⣴⠏⠁ ⣀⡤⢤⣀⣀ ⢀⣀⣤⣀⣀⡴⣄⡤⢤⣀⠤⠤⠴⣄⣀⡀ ",
" ⣀⣀ ⣠⣿⡍⣆ ⣠⣤⣤⠤⠴⠶⠖⠲⠤⠔⠛⠒⠉ ⠈⠨⣇⠖⠋ ⠈⠉⠓⠢⠤⢄ ",
" ⡀ ⣠⠤⠴⠒⠚⠛⠛⠒⠢⠤⠿⠙⠉⠉⠑⢋⣚⣉⠥⠚ ⢀⣀⡠⠟⠁ ⡴⠋ ",
" ⠐⠶⣛⣫⡤ ⠐⢏⣀⣤⣤ ⣴⣋⢇⢀⣮⡥ ⣴⠓ ",
"⠤⠤⠤⠤⡀⣈⢣⣠⡄ ⠉⠊⠉⠉⠉ ⠈⠓⠆⠤⠤",
" ",
]);
assert_eq!(buffer, expected);
}
}

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