Compare commits

..

391 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
montmorillonite
730dfd4940 docs(examples): show line gauge in demo example (#1309) 2024-08-07 20:25:43 -07:00
EdJoPaTo
097ee86e39 docs: remove superfluous doc(inline) (#1310)
It's no longer needed since #1260
2024-08-07 20:25:07 -07:00
Jack Wills
3fdb5e8987 docs: fix typo in terminal.rs (#1313) 2024-08-07 19:34:21 -07:00
Orhun Parmaksız
ec88bb81e5 chore(release): prepare for 0.28.0 (#1295)
🧀
2024-08-07 14:56:01 +03:00
Josh McKinney
f04bf855cb perf: add buffer benchmarks (#1303) 2024-08-06 23:05:36 -07:00
Alex Saveau
4753b7241b perf(reflow): eliminate most WordWrapper allocations (#1239)
On large paragraphs (~1MB), this saves hundreds of thousands of
allocations.

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

---------

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

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


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

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

---

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

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


</details>

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


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

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

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

---

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

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


</details>

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

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

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

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


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

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

---

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

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


</details>

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

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

---------

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

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

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

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

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

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

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

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

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

---

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

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

---------

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

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

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

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

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

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

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

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

---

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

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


</details>

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

---

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

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


</details>

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

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

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

---

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

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

</details>

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

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


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

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

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

---

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

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


</details>

---------

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

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

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

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

Includes rewording of the underline-color feature.

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

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

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

`layout::Corner` is removed entirely.

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

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

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

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

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

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

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

---------

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

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

Also simplify many tests involving buffer comparisons.

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

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

---

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

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

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

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

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

Fixes: #1068

---------

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

Fixes: #1062

---------

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


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

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

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

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

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

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

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


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

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

---

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

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


</details>

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

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

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

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

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

Are marked deprecated and replaced with the following

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

Example usage:

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

---------

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

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

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

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

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

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

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

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


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

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-02-05 16:25:33 -08:00
Jack Wills
14c67fbb52 fix(list): highlight symbol when using a multi-bytes char (#924)
ratatui v0.26.0 brought a regression in the List widget, in which the
highlight symbol width was incorrectly calculated - specifically when
the highlight symbol was a multi-char character, e.g. `▶`.
2024-02-05 20:59:19 +01:00
Mo
096346350e perf: Use drain instead of remove in chart examples (#922) 2024-02-05 04:54:05 -08:00
Valentin271
61a827821d docs(canvas): add documentation to canvas module (#913)
Document the whole `canvas` module. With this, the whole `widgets`
module is documented.
2024-02-03 13:58:29 -08:00
Dheepak Krishnamurthy
fbb5dfaaa9 fix: Scrollbar rendering when no track symbols are provided (#911) 2024-02-03 16:06:44 +01:00
299 changed files with 42431 additions and 25583 deletions

View File

@@ -1,16 +0,0 @@
#!/usr/bin/env bash
if !(command cargo-make >/dev/null 2>&1); then # Check if cargo-make is installed
echo Attempting to run cargo-make as part of the pre-push hook but it\'s not installed.
echo Please install it by running the following command:
echo
echo " cargo install --force cargo-make"
echo
echo If you don\'t want to run cargo-make as part of the pre-push hook, you can run
echo the following command instead of git push:
echo
echo " git push --no-verify"
exit 1
fi
cargo make ci

2
.cargo/config.toml Normal file
View File

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

View File

@@ -2,6 +2,12 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.rs]
indent_style = space
indent_size = 4

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
* @orhun @mindoodoo @sayanarijit @joshka @kdheepak @Valentin271
* @ratatui/maintainers

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

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

View File

@@ -2,7 +2,7 @@
name: Bug report
about: Create an issue about a bug you encountered
title: ''
labels: bug
labels: 'Type: Bug'
assignees: ''
---
@@ -17,26 +17,22 @@ A detailed and complete issue is more likely to be processed quickly.
A clear and concise description of what the bug is.
-->
## To Reproduce
<!--
Try to reduce the issue to a simple code sample exhibiting the problem.
Ideally, fork the project and add a test or an example.
-->
## Expected behavior
<!--
A clear and concise description of what you expected to happen.
-->
## Screenshots
<!--
If applicable, add screenshots, gifs or videos to help explain your problem.
-->
## Environment
<!--
Add a description of the systems where you are observing the issue. For example:

View File

@@ -1,5 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: Frequently Asked Questions
url: https://ratatui.rs/faq/
about: Check the website FAQ section to see if your question has already been answered
- name: Ratatui Forum
url: https://forum.ratatui.rs
about: Ask questions about ratatui on our Forum
- name: Discord Chat
url: https://discord.gg/pMCEU9hNEj
about: Ask questions about ratatui on Discord

View File

@@ -2,7 +2,7 @@
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
labels: 'Type: Enhancement'
assignees: ''
---

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

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

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

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

View File

@@ -0,0 +1,56 @@
name: Track Benchmarks with Bencher
on:
workflow_run:
workflows: [Run and Cache Benchmarks]
types: [completed]
permissions:
contents: read
pull-requests: write
jobs:
track_fork_pr_branch:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
env:
BENCHMARK_RESULTS: benchmark_results.txt
PR_EVENT: event.json
steps:
- name: Download Benchmark Results
uses: dawidd6/action-download-artifact@v6
with:
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.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);
- uses: bencherdev/bencher@main
- name: Track Benchmarks with Bencher
run: |
bencher run \
--project ratatui-org \
--token '${{ secrets.BENCHER_API_TOKEN }}' \
--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 "$PR_NUMBER" \
--file "$BENCHMARK_RESULTS"

View File

@@ -44,24 +44,6 @@ jobs:
header: pr-title-lint-error
delete: true
check-signed:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
# Check commit signature and add comment if needed
- name: Check signed commits in PR
uses: 1Password/check-signed-commits-action@v1
with:
comment: |
Thank you for opening this pull request!
We require commits to be signed and it looks like this PR contains unsigned commits.
Get help in the [CONTRIBUTING.md](https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md#sign-your-commits)
or on [Github doc](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits).
check-breaking-change-label:
runs-on: ubuntu-latest
env:
@@ -89,7 +71,7 @@ jobs:
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['breaking change']
labels: ['Type: Breaking Change']
})
do-not-merge:
@@ -102,4 +84,3 @@ jobs:
echo "Pull request is labeled as 'do not merge'"
echo "This workflow fails so that the pull request cannot be merged"
exit 1

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

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

View File

@@ -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,155 +16,170 @@ 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:
lint:
# Lint the formatting of the codebase.
lint-formatting:
name: Check Formatting
runs-on: ubuntu-latest
steps:
- name: Checkout
if: github.event_name != 'pull_request'
uses: actions/checkout@v4
- name: Checkout
if: github.event_name == 'pull_request'
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Check formatting
run: cargo make lint-format
- name: Check documentation
run: cargo make lint-docs
- name: Check conventional commits
uses: crate-ci/committed@master
with:
args: "-vv"
commits: HEAD
- name: Check typos
uses: crate-ci/typos@master
- name: Lint dependencies
uses: EmbarkStudios/cargo-deny-action@v1
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
with: { components: rustfmt }
- run: cargo xtask lint-formatting
clippy:
# Check for typos in the codebase.
# See <https://github.com/crate-ci/typos/>
lint-typos:
name: Check Typos
runs-on: ubuntu-latest
steps:
- name: Checkout
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
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Run cargo make clippy-all
run: cargo make clippy
- 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
# 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
- uses: Swatinem/rust-cache@v2
- uses: bnjbvr/cargo-machete@v0.7.0
# Run cargo clippy.
lint-clippy:
name: Check Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- 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:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- uses: actions/checkout@v4
- 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
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Generate coverage
run: cargo make coverage
- name: Upload to codecov.io
uses: codecov/codecov-action@v3
- uses: taiki-e/install-action@cargo-llvm-cov
- uses: Swatinem/rust-cache@v2
- 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:
os: [ubuntu-latest, windows-latest, macos-latest]
toolchain: ["1.70.0", "stable"]
toolchain: ["1.74.0", "stable"]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust {{ matrix.toolchain }}
uses: dtolnay/rust-toolchain@master
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Run cargo make check
run: cargo make check
env:
RUST_BACKTRACE: full
- uses: Swatinem/rust-cache@v2
- run: cargo xtask check
test-doc:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
# Check if README.md is up-to-date with the crate's documentation.
check-readme:
name: Check README
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Test docs
run: cargo make test-doc
env:
RUST_BACKTRACE: full
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@cargo-rdme
- run: cargo xtask check-readme
test:
# 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
- uses: dtolnay/rust-toolchain@nightly
- uses: dtolnay/install@cargo-docs-rs
- uses: Swatinem/rust-cache@v2
- run: cargo xtask lint-docs
# 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.70.0", "stable"]
backend: [crossterm, termion, termwiz]
exclude:
# termion is not supported on windows
- os: windows-latest
backend: termion
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
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
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Test ${{ matrix.backend }}
run: cargo make test-backend ${{ matrix.backend }}
env:
RUST_BACKTRACE: full
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- 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@v2
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

@@ -2,14 +2,41 @@
This document contains a list of breaking changes in each version and some notes to help migrate
between versions. It is compiled manually from the commit history and changelog. We also tag PRs on
github with a [breaking change] label.
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` trait migrates to `get/set_cursor_position`
- Ratatui now requires Crossterm 0.28.0
- `Axis::labels` now accepts `IntoIterator<Into<Line>>`
- `Layout::init_cache` no longer returns bool and takes a `NonZeroUsize` instead of `usize`
- `ratatui::terminal` module is now private
- `ToText` no longer has a lifetime
- `Frame::size` is deprecated and renamed to `Frame::area`
- [v0.27.0](#v0270)
- List no clamps the selected index to list
- Prelude items added / removed
- 'termion' updated to 4.0
- `Rect::inner` takes `Margin` directly instead of reference
- `Buffer::filled` takes `Cell` directly instead of reference
- `Stylize::bg()` now accepts `Into<Color>`
- Removed deprecated `List::start_corner`
- `LineGauge::gauge_style` is deprecated
- [v0.26.0](#v0260)
- `Flex::Start` is the new default flex mode for `Layout`
- `patch_style` & `reset_style` now consume and return `Self`
@@ -37,7 +64,7 @@ This is a quick summary of the sections below:
- `Scrollbar`: symbols moved to `symbols` module
- MSRV is now 1.67.0
- [v0.22.0](#v0220)
- serde representation of `Borders` and `Modifiers` has changed
- `serde` representation of `Borders` and `Modifiers` has changed
- [v0.21.0](#v0210)
- MSRV is now 1.65.0
- `terminal::ViewPort` is now an enum
@@ -47,21 +74,413 @@ This is a quick summary of the sections below:
- MSRV is now 1.63.0
- `List` no longer ignores empty strings
## [v0.26.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.26.0)
## Unreleased (0.30.0)
### `Flex::Start` is the new default flex mode for `Layout`
### `WidgetRef` no longer has a blanket implementation of Widget
[#881]: https://github.com/ratatui-org/ratatui/pull/881
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.
Previously, constraints would stretch to fill all available space, violating constraints if
```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/ratatui/pull/1254
The `Backend::size` method returns a `Size` instead of a `Rect`.
There is no need for the position here as it was always 0,0.
### `Backend` trait migrates to `get/set_cursor_position` ([#1284])
[#1284]: https://github.com/ratatui/ratatui/pull/1284
If you just use the types implementing the `Backend` trait, you will see deprecation hints but
nothing is a breaking change for you.
If you implement the Backend trait yourself, you need to update the implementation and add the
`get/set_cursor_position` method. You can remove the `get/set_cursor` methods as they are deprecated
and a default implementation for them exists.
### Ratatui now requires Crossterm 0.28.0 ([#1278])
[#1278]: https://github.com/ratatui/ratatui/pull/1278
Crossterm is updated to version 0.28.0, which is a semver incompatible version with the previous
version (0.27.0). Ratatui re-exports the version of crossterm that it is compatible with under
`ratatui::crossterm`, which can be used to avoid incompatible versions in your dependency list.
### `Axis::labels()` now accepts `IntoIterator<Into<Line>>` ([#1273] and [#1283])
[#1273]: https://github.com/ratatui/ratatui/pull/1173
[#1283]: https://github.com/ratatui/ratatui/pull/1283
Previously Axis::labels accepted `Vec<Span>`. Any code that uses conversion methods that infer the
type will need to be rewritten as the compiler cannot infer the correct type.
```diff
- Axis::default().labels(vec!["a".into(), "b".into()])
+ Axis::default().labels(["a", "b"])
```
### `Layout::init_cache` no longer returns bool and takes a `NonZeroUsize` instead of `usize` ([#1245])
[#1245]: https://github.com/ratatui/ratatui/pull/1245
```diff
- let is_initialized = Layout::init_cache(100);
+ Layout::init_cache(NonZeroUsize::new(100).unwrap());
```
### `ratatui::terminal` module is now private ([#1160])
[#1160]: https://github.com/ratatui/ratatui/pull/1160
The `terminal` module is now private and can not be used directly. The types under this module are
exported from the root of the crate. This reduces clashes with other modules in the backends that
are also named terminal, and confusion about module exports for newer Rust users.
```diff
- use ratatui::terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
+ use ratatui::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
```
### `ToText` no longer has a lifetime ([#1234])
[#1234]: https://github.com/ratatui/ratatui/pull/1234
This change simplifies the trait and makes it easier to implement.
### `Frame::size` is deprecated and renamed to `Frame::area` ([#1293])
[#1293]: https://github.com/ratatui/ratatui/pull/1293
`Frame::size` is renamed to `Frame::area` as it's the more correct name.
## [v0.27.0](https://github.com/ratatui/ratatui/releases/tag/v0.27.0)
### List no clamps the selected index to list ([#1159])
[#1149]: https://github.com/ratatui/ratatui/pull/1149
The `List` widget now clamps the selected index to the bounds of the list when navigating with
`first`, `last`, `previous`, and `next`, as well as when setting the index directly with `select`.
Previously selecting an index past the end of the list would show treat the list as having a
selection which was not visible. Now the last item in the list will be selected instead.
### Prelude items added / removed ([#1149])
The following items have been removed from the prelude:
- `style::Styled` - this trait is useful for widgets that want to
support the Stylize trait, but it adds complexity as widgets have two
`style` methods and a `set_style` method.
- `symbols::Marker` - this item is used by code that needs to draw to
the `Canvas` widget, but it's not a common item that would be used by
most users of the library.
- `terminal::{CompletedFrame, TerminalOptions, Viewport}` - these items
are rarely used by code that needs to interact with the terminal, and
they're generally only ever used once in any app.
The following items have been added to the prelude:
- `layout::{Position, Size}` - these items are used by code that needs
to interact with the layout system. These are newer items that were
added in the last few releases, which should be used more liberally.
This may cause conflicts for types defined elsewhere with a similar
name.
To update your app:
```diff
// if your app uses Styled::style() or Styled::set_style():
-use ratatui::prelude::*;
+use ratatui::{prelude::*, style::Styled};
// if your app uses symbols::Marker:
-use ratatui::prelude::*;
+use ratatui::{prelude::*, symbols::Marker}
// if your app uses terminal::{CompletedFrame, TerminalOptions, Viewport}
-use ratatui::prelude::*;
+use ratatui::{prelude::*, terminal::{CompletedFrame, TerminalOptions, Viewport}};
// to disambiguate existing types named Position or Size:
- use some_crate::{Position, Size};
- let size: Size = ...;
- let position: Position = ...;
+ let size: some_crate::Size = ...;
+ let position: some_crate::Position = ...;
```
### Termion is updated to 4.0 [#1106]
Changelog: <https://gitlab.redox-os.org/redox-os/termion/-/blob/master/CHANGELOG.md>
A change is only necessary if you were matching on all variants of the `MouseEvent` enum without a
wildcard. In this case, you need to either handle the two new variants, `MouseLeft` and
`MouseRight`, or add a wildcard.
[#1106]: https://github.com/ratatui/ratatui/pull/1106
### `Rect::inner` takes `Margin` directly instead of reference ([#1008])
[#1008]: https://github.com/ratatui/ratatui/pull/1008
`Margin` needs to be passed without reference now.
```diff
-let area = area.inner(&Margin {
+let area = area.inner(Margin {
vertical: 0,
horizontal: 2,
});
```
### `Buffer::filled` takes `Cell` directly instead of reference ([#1148])
[#1148]: https://github.com/ratatui/ratatui/pull/1148
`Buffer::filled` moves the `Cell` instead of taking a reference.
```diff
-Buffer::filled(area, &Cell::new("X"));
+Buffer::filled(area, Cell::new("X"));
```
### `Stylize::bg()` now accepts `Into<Color>` ([#1103])
[#1103]: https://github.com/ratatui/ratatui/pull/1103
Previously, `Stylize::bg()` accepted `Color` but now accepts `Into<Color>`. This allows more
flexible types from calling scopes, though it can break some type inference in the calling scope.
### Remove deprecated `List::start_corner` and `layout::Corner` ([#759])
[#759]: https://github.com/ratatui/ratatui/pull/759
`List::start_corner` was deprecated in v0.25. Use `List::direction` and `ListDirection` instead.
```diff
- list.start_corner(Corner::TopLeft);
- list.start_corner(Corner::TopRight);
// This is not an error, BottomRight rendered top to bottom previously
- list.start_corner(Corner::BottomRight);
// all becomes
+ list.direction(ListDirection::TopToBottom);
```
```diff
- list.start_corner(Corner::BottomLeft);
// becomes
+ list.direction(ListDirection::BottomToTop);
```
`layout::Corner` was removed entirely.
### `LineGauge::gauge_style` is deprecated ([#565])
[#565]: https://github.com/ratatui/ratatui/pull/1148
`LineGauge::gauge_style` is deprecated and replaced with `LineGauge::filled_style` and `LineGauge::unfilled_style`:
```diff
let gauge = LineGauge::default()
- .gauge_style(Style::default().fg(Color::Red).bg(Color::Blue)
+ .filled_style(Style::default().fg(Color::Green))
+ .unfilled_style(Style::default().fg(Color::White));
```
## [v0.26.0](https://github.com/ratatui/ratatui/releases/tag/v0.26.0)
### `Flex::Start` is the new default flex mode for `Layout` ([#881])
[#881]: https://github.com/ratatui/ratatui/pull/881
Previously, constraints would stretch to fill all available space, violating constraints if
necessary.
With v0.26.0, `Flex` modes are introduced and the default is `Flex::Start`, which will align
areas associated with constraints to be beginning of the area. With v0.26.0, additionally,
`Min` constraints grow to fill excess space. These changes will allow users to build layouts
With v0.26.0, `Flex` modes are introduced, and the default is `Flex::Start`, which will align
areas associated with constraints to be beginning of the area. With v0.26.0, additionally,
`Min` constraints grow to fill excess space. These changes will allow users to build layouts
more easily.
With v0.26.0, users will most likely not need to change what constraints they use to create
With v0.26.0, users will most likely not need to change what constraints they use to create
existing layouts with `Flex::Start`. However, to get old behavior, use `Flex::Legacy`.
```diff
@@ -72,9 +491,9 @@ 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
Previously, `Table::new()` accepted `IntoIterator<Item=Row<'a>>`. The argument change to
`IntoIterator<Item: Into<Row<'a>>>`, This allows more flexible types from calling scopes, though it
can some break type inference in the calling scope for empty containers.
@@ -89,9 +508,9 @@ 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
Previously, `Tabs::new()` accepted `Vec<T>` where `T: Into<Line<'a>>`. This allows more flexible
types from calling scopes, though it can break some type inference in the calling scope.
This typically occurs when collecting an iterator prior to calling `Tabs::new`, and can be resolved
@@ -105,17 +524,17 @@ 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.
field to `SegmentSize::None`. This will affect the rendering of a small amount of apps.
To use the previous default values, call `table.segment_size(Default::default())` and
`table.column_spacing(0)`.
### `patch_style` & `reset_style` now consumes and returns `Self` ([#754])
[#754]: https://github.com/ratatui-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
@@ -132,8 +551,6 @@ The following example shows how to migrate for `Line`, but the same applies for
### Remove deprecated `Block::title_on_bottom` ([#757])
[#757]: https://github.com/ratatui-org/ratatui/pull/757
`Block::title_on_bottom` was deprecated in v0.22. Use `Block::title` and `Title::position` instead.
```diff
@@ -143,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
@@ -151,10 +568,10 @@ 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::style` method. Any code that creates
itself has a `style` field, which can be set with the `Line::styled` method. Any code that creates
`Line`s using the struct initializer instead of constructors will fail to compile due to the added
field. This can be easily fixed by adding `..Default::default()` to the field list or by using a
constructor method (`Line::styled()`, `Line::raw()`) or conversion method (`Line::from()`).
@@ -175,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.
@@ -193,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).
@@ -208,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.
@@ -220,10 +637,10 @@ 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:
Previously `Table`s could be constructed without `widths`. In almost all cases this is an error.
A new `widths` parameter is now mandatory on `Table::new()`. Existing code of the form:
```diff
- Table::new(rows).widths(widths)
@@ -246,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
@@ -262,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.
@@ -279,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])
### `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
@@ -304,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
@@ -318,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
@@ -336,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`.
@@ -349,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.
@@ -365,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
@@ -379,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])
### `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
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.
@@ -420,11 +837,11 @@ 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 did
A new type `Masked` was introduced that implements `From<Text<'a>>`. This causes any code that
previously did not need to use type annotations to fail to compile. To fix this, annotate or call
to_string() / to_owned() / as_str() on the value. E.g.:
`to_string()` / `to_owned()` / `as_str()` on the value. E.g.:
```diff
- let paragraph = Paragraph::new("".as_ref());
@@ -434,7 +851,7 @@ to_string() / to_owned() / as_str() on the value. E.g.:
### `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
@@ -446,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

128
CODE_OF_CONDUCT.md Normal file
View File

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

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.
@@ -17,10 +17,10 @@ found.
All contributions are obviously welcome. Please include as many details as possible in your PR
description to help the reviewer (follow the provided template). Make sure to highlight changes
which may need additional attention or you are uncertain about. Any idea with a large scale impact
which may need additional attention, or you are uncertain about. Any idea with a large scale impact
on the crate or its users should ideally be discussed in a "Feature Request" issue beforehand.
### Keep PRs small, intentional and focused
### Keep PRs small, intentional, and focused
Try to do one pull request per change. The time taken to review a PR grows exponential with the size
of the change. Small focused PRs will generally be much more faster to review. PRs that include both
@@ -31,8 +31,8 @@ guarantee that the behavior is unchanged.
### Code formatting
Run `cargo make format` before committing to ensure that code is consistently formatted with
rustfmt. Configuration is in [rustfmt.toml](./rustfmt.toml).
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,11 +56,9 @@ documented.
### Run CI tests before pushing a PR
We're using [cargo-husky](https://github.com/rhysd/cargo-husky) to automatically run git hooks,
which will run `cargo make ci` before each push. To initialize the hook run `cargo test`. If
`cargo-make` is not installed, it will provide instructions to install it for you. This will ensure
that your code is formatted, compiles and passes all tests before you push. If you need to skip this
check, you can use `git push --no-verify`.
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.
### Sign your commits
@@ -73,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
@@ -112,7 +110,8 @@ exist to show coverage directly in your editor. E.g.:
### Documentation
Here are some guidelines for writing documentation in Ratatui.
Here are some guidelines for writing documentation in Ratatui.
Every public API **must** be documented.
Keep in mind that Ratatui tends to attract beginner Rust users that may not be familiar with Rust
@@ -125,10 +124,9 @@ the concepts pointing to the various methods. Focus on interaction with various
enough information that helps understand why you might want something.
Examples should help users understand a particular usage, not test a feature. They should be as
simple as possible.
Prefer hiding imports and using wildcards to keep things concise. Some imports may still be shown
to demonstrate a particular non-obvious import (e.g. `Stylize` trait to use style methods).
Speaking of `Stylize`, you should use it over the more verbose style setters:
simple as possible. Prefer hiding imports and using wildcards to keep things concise. Some imports
may still be shown to demonstrate a particular non-obvious import (e.g. `Stylize` trait to use style
methods). Speaking of `Stylize`, you should use it over the more verbose style setters:
```rust
let style = Style::new().red().bold();
@@ -138,7 +136,7 @@ let style = Style::default().fg(Color::Red).add_modifier(Modifiers::BOLD);
#### Format
- First line is summary, second is blank, third onward is more detail
- First line is summary, second is blank, third onward is more detail
```rust
/// Summary
@@ -148,10 +146,10 @@ let style = Style::default().fg(Color::Red).add_modifier(Modifiers::BOLD);
fn foo() {}
```
- Max line length is 100 characters
See [vscode rewrap extension](https://marketplace.visualstudio.com/items?itemName=stkb.rewrap)
- Max line length is 100 characters
See [VS Code rewrap extension](https://marketplace.visualstudio.com/items?itemName=stkb.rewrap)
- Doc comments are above macros
- Doc comments are above macros
i.e.
```rust
@@ -160,31 +158,31 @@ i.e.
struct Foo {}
```
- Code items should be between backticks
- Code items should be between backticks
i.e. ``[`Block`]``, **NOT** ``[Block]``
### Deprecation notice
We generally want to wait at least two versions before removing deprecated items so users have
We generally want to wait at least two versions before removing deprecated items, so users have
time to update. However, if a deprecation is blocking for us to implement a new feature we may
*consider* removing it in a one version notice.
### Use of unsafe for optimization purposes
We don't currently use any unsafe code in Ratatui, and would like to keep it that way. However there
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
We use Github Actions for the CI where we perform the following checks:
We use GitHub Actions for the CI where we perform the following checks:
- The code should compile on `stable` and the Minimum Supported Rust Version (MSRV).
- The tests (docs, lib, tests and examples) should pass.
- The code should conform to the default format enforced by `rustfmt`.
- The code should not contain common style issues `clippy`.
You can also check most of those things yourself locally using `cargo make ci` which will offer you
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`
@@ -193,12 +191,12 @@ This project was forked from [`tui-rs`](https://github.com/fdehau/tui-rs/) in Fe
[blessing of the original author](https://github.com/fdehau/tui-rs/issues/654), Florian Dehau
([@fdehau](https://github.com/fdehau)).
The original repository contains all the issues, PRs and discussion that were raised originally, and
The original repository contains all the issues, PRs, and discussion that were raised originally, and
it is useful to refer to when contributing code, documentation, or issues with Ratatui.
We imported all the PRs from the original repository and implemented many of the smaller ones and
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,294 +1,55 @@
[package]
name = "ratatui"
version = "0.26.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/ratatui"
homepage = "https://ratatui.rs"
keywords = ["tui", "terminal", "dashboard"]
repository = "https://github.com/ratatui-org/ratatui"
categories = ["command-line-interface"]
readme = "README.md"
license = "MIT"
exclude = [
"assets/*",
".github",
"Makefile.toml",
"CONTRIBUTING.md",
"*.log",
"tags",
]
autoexamples = true
exclude = ["assets/*", ".github", "Makefile.toml", "CONTRIBUTING.md", "*.log", "tags"]
edition = "2021"
rust-version = "1.70.0"
rust-version = "1.74.0"
[badges]
[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"
[dependencies]
crossterm = { version = "0.27", optional = true }
termion = { version = "3.0", optional = true }
termwiz = { version = "0.22.0", optional = true }
serde = { version = "1", optional = true, features = ["derive"] }
bitflags = "2.3"
cassowary = "0.3"
indoc = "2.0"
itertools = "0.12"
paste = "1.0.2"
strum = { version = "0.26", features = ["derive"] }
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
unicode-segmentation = "1.10"
unicode-width = "0.1"
document-features = { version = "0.2.7", optional = true }
lru = "0.12.0"
stability = "0.1.1"
compact_str = "0.7.1"
[dev-dependencies]
anyhow = "1.0.71"
argh = "0.1.12"
better-panic = "0.3.0"
cargo-husky = { version = "1.5.0", default-features = false, features = [
"user-hooks",
] }
color-eyre = "0.6.2"
criterion = { version = "0.5.1", features = ["html_reports"] }
derive_builder = "0.13.0"
fakeit = "1.1"
font8x8 = "0.3.1"
palette = "0.7.3"
pretty_assertions = "1.4.0"
rand = "0.8.5"
rand_chacha = "0.3.1"
rstest = "0.18.2"
serde_json = "1.0.109"
[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 and adds a dependency on the [Crossterm crate].
crossterm = ["dep:crossterm"]
## enables the [`TermionBackend`] backend and adds a dependency on the [Termion crate].
termion = ["dep:termion"]
## enables the [`TermwizBackend`] backend and adds a dependency on the [Termwiz crate].
termwiz = ["dep:termwiz"]
#! The following optional features are available for all backends:
## enables serialization and deserialization of style and color types using the [Serde crate].
## This is useful if you want to save themes to a file.
serde = ["dep:serde", "bitflags/serde", "compact_str/serde"]
## enables the [`border!`] macro.
macros = []
## 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`] widget module and adds a dependency on the [Time crate].
widget-calendar = ["dep:time"]
#! Underline color is only supported by the [`CrosstermBackend`] backend, and is not supported
#! on Windows 7.
## enables the backend code that sets the underline color.
underline-color = ["dep:crossterm"]
#! The following features are unstable and may change in the future:
## Enable all unstable features.
unstable = ["unstable-rendered-line-info", "unstable-widget-ref"]
## Enables the [`Paragraph::line_count`](crate::widgets::Paragraph::line_count)
## [`Paragraph::line_width`](crate::widgets::Paragraph::line_width) methods
## which are experimental and may change in the future.
## See [Issue 293](https://github.com/ratatui-org/ratatui/issues/293) for more details.
unstable-rendered-line-info = []
## Enables the `WidgetRef` and `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"]
[[bench]]
name = "barchart"
harness = false
[[bench]]
name = "block"
harness = false
[[bench]]
name = "list"
harness = false
[lib]
bench = false
[[bench]]
name = "paragraph"
harness = false
[[bench]]
name = "sparkline"
harness = false
[[example]]
name = "barchart"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "block"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "canvas"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "calendar"
required-features = ["crossterm", "widget-calendar"]
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"]
doc-scrape-examples = true
[[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", "widget-calendar"]
doc-scrape-examples = true
[[example]]
name = "docsrs"
required-features = ["crossterm"]
doc-scrape-examples = false
[[example]]
name = "gauge"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "hello_world"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "layout"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "constraints"
required-features = ["crossterm"]
doc-scrape-examples = false
[[example]]
name = "flex"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "constraint-explorer"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "list"
required-features = ["crossterm"]
doc-scrape-examples = true
[[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 = "user_input"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "inline"
required-features = ["crossterm"]
doc-scrape-examples = true
[[test]]
name = "state_serde"
required-features = ["serde"]
# Improve benchmark consistency
[profile.bench]
codegen-units = 1
lto = true

7
FUNDING.json Normal file
View File

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

View File

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

15
MAINTAINERS.md Normal file
View File

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

View File

@@ -1,174 +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"
# Windows does not support building termion, so this avoids the build failure by providing two
# sets of flags, one for Windows and one for other platforms.
# Windows: --features=all-widgets,macros,serde,crossterm,termwiz,underline-color
# Other: --features=all-widgets,macros,serde,crossterm,termion,termwiz,underline-color
ALL_FEATURES_FLAG = { source = "${CARGO_MAKE_RUST_TARGET_OS}", default_value = "--features=all-widgets,macros,serde,crossterm,termion,termwiz,unstable", mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz,unstable" } }
[tasks.default]
alias = "ci"
[tasks.ci]
description = "Run continuous integration tasks"
dependencies = ["lint-style", "clippy", "check", "test"]
[tasks.lint-style]
description = "Lint code style (formatting, typos, docs)"
dependencies = ["lint-format", "lint-typos", "lint-docs"]
[tasks.lint-format]
description = "Lint code formatting"
toolchain = "nightly"
command = "cargo"
args = ["fmt", "--all", "--check"]
[tasks.format]
description = "Fix code formatting"
toolchain = "nightly"
command = "cargo"
args = ["fmt", "--all"]
[tasks.lint-typos]
description = "Run typo checks"
install_crate = { crate_name = "typos-cli", binary = "typos", test_arg = "--version" }
command = "typos"
[tasks.lint-docs]
description = "Check documentation for errors and warnings"
toolchain = "nightly"
command = "cargo"
args = [
"rustdoc",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
"--",
"-Zunstable-options",
"--check",
"-Dwarnings",
]
[tasks.check]
description = "Check code for errors and warnings"
command = "cargo"
args = [
"check",
"--all-targets",
"--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
'''

483
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,14 +24,14 @@
<!-- cargo-rdme start -->
![Demo](https://github.com/ratatui-org/ratatui/blob/1d39444e3dea6f309cf9035be2417ac711c1abc9/examples/demo2-destroy.gif?raw=true)
![Demo](https://github.com/ratatui/ratatui/blob/87ae72dbc756067c97f6400d3e2a58eeb383776e/examples/demo2-destroy.gif?raw=true)
<div align="center">
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
Badge]](./LICENSE)<br>
[![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
[![Matrix Badge]][Matrix]<br>
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![Deps.rs
Badge]][Deps.rs]<br> [![Codecov Badge]][Codecov] [![License Badge]](./LICENSE) [![Sponsors
Badge]][GitHub Sponsors]<br> [![Discord Badge]][Discord Server] [![Matrix Badge]][Matrix]
[![Forum Badge]][Forum]<br>
[Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br>
[Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request]
@@ -41,7 +44,7 @@ Badge]](./LICENSE)<br>
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` and `crossterm` as dependencies to your cargo.toml:
@@ -49,9 +52,47 @@ Add `ratatui` and `crossterm` as dependencies to your cargo.toml:
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:
```rust
use crossterm::event::{self, Event};
use ratatui::{text::Text, Frame};
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();
}
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
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
- [Ratatui Forum][Forum] - a place to ask questions and discuss the library
- [API Docs] - the full API documentation for the library on docs.rs.
- [Examples] - a collection of examples that demonstrate how to use the library.
- [Contributing] - Please read this if you are interested in contributing to the project.
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
- [Breaking Changes] - a list of breaking changes in the library.
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.
## Introduction
@@ -61,54 +102,71 @@ This is in contrast to the retained mode style of rendering where widgets are up
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
for more info.
## Other documentation
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
- [API Docs] - the full API documentation for the library on docs.rs.
- [Examples] - a collection of examples that demonstrate how to use the library.
- [Contributing] - Please read this if you are interested in contributing to the project.
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
- [Breaking Changes] - a list of breaking changes in the library.
## Quickstart
The following example demonstrates the minimal amount of code necessary to setup a terminal and
render "Hello World!". The full code for this example which contains a little more detail is in
the [Examples] directory. For more guidance on different ways to structure your application see
the [Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the
various [Examples]. There are also several starter templates available in the [templates]
repository.
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. 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
@@ -117,58 +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 crossterm::{
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{prelude::*, widgets::*};
fn main() -> io::Result<()> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
let mut should_quit = false;
while !should_quit {
terminal.draw(ui)?;
should_quit = handle_events()?;
}
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
fn 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);
}
}
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
_ => {}
},
// handle other events
_ => {}
}
Ok(false)
}
fn ui(frame: &mut Frame) {
frame.render_widget(
Paragraph::new("Hello World!")
.block(Block::default().title("Greeting").borders(Borders::ALL)),
frame.size(),
);
}
```
Running this example produces the following output:
![docsrs-hello]
## Layout
The library comes with a basic yet useful layout management object called [`Layout`] which
@@ -177,46 +200,36 @@ area. This lets you describe a responsive terminal UI by nesting layouts. See th
section of the [Ratatui Website] for more info.
```rust
use ratatui::{prelude::*, widgets::*};
use ratatui::{
layout::{Constraint, Layout},
widgets::Block,
Frame,
};
fn ui(frame: &mut Frame) {
let main_layout = Layout::new(
Direction::Vertical,
[
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
],
)
.split(frame.size());
frame.render_widget(
Block::new().borders(Borders::TOP).title("Title Bar"),
main_layout[0],
);
frame.render_widget(
Block::new().borders(Borders::TOP).title("Status Bar"),
main_layout[2],
);
fn draw(frame: &mut Frame) {
use Constraint::{Fill, Length, Min};
let inner_layout = Layout::new(
Direction::Horizontal,
[Constraint::Percentage(50), Constraint::Percentage(50)],
)
.split(main_layout[1]);
frame.render_widget(
Block::default().borders(Borders::ALL).title("Left"),
inner_layout[0],
);
frame.render_widget(
Block::default().borders(Borders::ALL).title("Right"),
inner_layout[1],
);
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);
frame.render_widget(Block::bordered().title("Left"), left_area);
frame.render_widget(Block::bordered().title("Right"), right_area);
}
```
Running this example produces the following output:
![docsrs-layout]
```text
Title Bar───────────────────────────────────
┌Left────────────────┐┌Right───────────────┐
│ ││ │
└────────────────────┘└────────────────────┘
Status Bar──────────────────────────────────
```
## Text and styling
@@ -231,55 +244,44 @@ short-hand syntax to apply a style to widgets and text. See the [Styling Text] s
[Ratatui Website] for more info.
```rust
use ratatui::{prelude::*, widgets::*};
use ratatui::{
layout::{Constraint, Layout},
style::{Color, Modifier, Style, Stylize},
text::{Line, Span},
widgets::{Block, Paragraph},
Frame,
};
fn ui(frame: &mut Frame) {
let areas = Layout::new(
Direction::Vertical,
[
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
],
)
.split(frame.size());
fn draw(frame: &mut Frame) {
let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.area());
let span1 = Span::raw("Hello ");
let span2 = Span::styled(
"World",
Style::new()
.fg(Color::Green)
.bg(Color::White)
.add_modifier(Modifier::BOLD),
);
let span3 = "!".red().on_light_yellow().italic();
let line = Line::from(vec![
Span::raw("Hello "),
Span::styled(
"World",
Style::new()
.fg(Color::Green)
.bg(Color::White)
.add_modifier(Modifier::BOLD),
),
"!".red().on_light_yellow().italic(),
]);
frame.render_widget(line, areas[0]);
let line = Line::from(vec![span1, span2, span3]);
let text: Text = Text::from(vec![line]);
// using the short-hand syntax and implicit conversions
let paragraph = Paragraph::new("Hello World!".red().on_white().bold());
frame.render_widget(paragraph, areas[1]);
frame.render_widget(Paragraph::new(text), areas[0]);
// or using the short-hand syntax and implicit conversions
frame.render_widget(
Paragraph::new("Hello World!".red().on_white().bold()),
areas[1],
);
// style the whole widget instead of just the text
let paragraph = Paragraph::new("Hello World!").style(Style::new().red().on_white());
frame.render_widget(paragraph, areas[2]);
// to style the whole widget instead of just the text
frame.render_widget(
Paragraph::new("Hello World!").style(Style::new().red().on_white()),
areas[2],
);
// or using the short-hand syntax
frame.render_widget(Paragraph::new("Hello World!").blue().on_yellow(), areas[3]);
// use the simpler short-hand syntax
let paragraph = Paragraph::new("Hello World!").blue().on_yellow();
frame.render_widget(paragraph, areas[3]);
}
```
Running this example produces the following output:
![docsrs-styling]
[Ratatui Website]: https://ratatui.rs/
[Installation]: https://ratatui.rs/installation/
[Rendering]: https://ratatui.rs/concepts/rendering/
@@ -290,20 +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
[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
[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
[`Frame`]: terminal::Frame
[`render_widget`]: terminal::Frame::render_widget
[`Widget`]: widgets::Widget
@@ -322,119 +322,58 @@ 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
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
[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
[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
[Discord Badge]:
https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square
[GitHub Sponsors]: https://github.com/sponsors/ratatui
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square&logoColor=E05D44&color=E05D44
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square&color=1370D3
[CI Badge]: https://img.shields.io/github/actions/workflow/status/ratatui/ratatui/ci.yml?style=flat-square&logo=github
[CI Workflow]: https://github.com/ratatui/ratatui/actions/workflows/ci.yml
[Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
[Codecov]: https://app.codecov.io/gh/ratatui/ratatui
[Deps.rs Badge]: https://deps.rs/repo/github/ratatui/ratatui/status.svg?style=flat-square
[Deps.rs]: https://deps.rs/repo/github/ratatui/ratatui
[Discord Badge]: https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3
[Discord Server]: https://discord.gg/pMCEU9hNEj
[Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square
[Matrix Badge]:
https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix
[Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square&logoColor=E05D44
[Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
[Forum Badge]: https://img.shields.io/discourse/likes?server=https%3A%2F%2Fforum.ratatui.rs&style=flat-square&logo=discourse&label=forum&color=C43AC3
[Forum]: https://forum.ratatui.rs
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui?logo=github&style=flat-square&color=1370D3
<!-- cargo-rdme end -->
## Status of this fork
In response to the original maintainer [**Florian Dehau**](https://github.com/fdehau)'s issue
regarding the [future of `tui-rs`](https://github.com/fdehau/tui-rs/issues/654), several members of
the community forked the project and created this crate. We look forward to continuing the work
started by Florian 🚀
## 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.
Our primary open-source workflow is centered around GitHub.
For significant discussions, we rely on GitHub — please open an issue, a discussion or a PR.
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.
## Rust version requirements
## Built with Ratatui
Since version 0.23.0, The Minimum Supported Rust Version (MSRV) of `ratatui` is 1.67.0.
## Widgets
### Built in
The library comes with the following
[widgets](https://docs.rs/ratatui/latest/ratatui/widgets/index.html):
- [BarChart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
- [Block](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html)
- [Calendar](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
- [Canvas](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/struct.Canvas.html) which allows
rendering [points, lines, shapes and a world
map](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html)
- [Chart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Chart.html)
- [Clear](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Clear.html)
- [Gauge](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Gauge.html)
- [List](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html)
- [Paragraph](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Paragraph.html)
- [Scrollbar](https://docs.rs/ratatui/latest/ratatui/widgets/scrollbar/struct.Scrollbar.html)
- [Sparkline](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Sparkline.html)
- [Table](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Table.html)
- [Tabs](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Tabs.html)
Each widget has an associated example which can be found in the [Examples] folder. Run each example
with cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by pressing `q`.
You can also run all examples by running `cargo make run-examples` (requires `cargo-make` that can
be installed with `cargo install cargo-make`).
### Third-party libraries, bootstrapping templates and widgets
- [ansi-to-tui](https://github.com/uttarayan21/ansi-to-tui) — Convert ansi colored text to
`ratatui::text::Text`
- [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
`ratatui::style::Color`
- [templates](https://github.com/ratatui-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,48 +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"]
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"
@@ -59,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",
]
@@ -72,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",
"--all-features",
"--color", "always",
"cargo",
"llvm-cov",
"--lcov",
"--output-path",
"target/lcov.info",
"--all-features",
]
[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
@@ -92,9 +127,11 @@ 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"
v = "job:coverage"
u = "job:coverage-unit-tests-only"
ctrl-v = "job:coverage-unit-tests-only"
u = "job:test-unit"
n = "job:nextest"

View File

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

View File

@@ -1,4 +1,9 @@
# configuration for https://github.com/orhun/git-cliff
# git-cliff ~ configuration file
# https://git-cliff.org/docs/configuration
[remote.github]
owner = "ratatui"
repo = "ratatui"
[changelog]
# changelog header
@@ -6,6 +11,8 @@ header = """
# Changelog
All notable changes to this project will be documented in this file.
<!-- ignore lint rules that are often triggered by content generated from commits / git-cliff -->
<!-- markdownlint-disable line-length no-bare-urls ul-style emphasis-style -->
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
@@ -17,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.scope | default(value = "uncategorized") | lower }})* {{ commit.message | upper_first }}
- [{{ 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.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 %}
````text {#- 4 backticks escape any backticks in body #}
{{commit.body | indent(prefix=" ") }}
````
{%- endif %}
{%- for footer in commit.footers %}
{%- if footer.token != "Signed-off-by" and footer.token != "Co-authored-by" %}
{{ footer.token | indent(prefix=" ") }}{{ footer.separator }}{{ footer.value }}
{%- endif %}
{%- endfor %}
{% endmacro -%}
{% for group, commits in commits | group_by(attribute="group") %}
@@ -49,6 +46,28 @@ body = """
{%- endif -%}
{%- endfor -%}
{%- endfor %}
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
### New Contributors
{%- endif %}\
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
* @{{ contributor.username }} made their first contribution
{%- if contributor.pr_number %} in \
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
{%- endif %}
{%- endfor -%}
{% if version %}
{% if previous.version %}
**Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}
{% endif %}
{% else -%}
{% raw %}\n{% endraw %}
{% endif %}
{%- macro remote_url() -%}
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\
{% endmacro %}
"""
@@ -58,6 +77,11 @@ trim = false
footer = """
<!-- generated by git-cliff -->
"""
postprocessors = [
{ pattern = '<!-- Please read CONTRIBUTING.md before submitting any pull request. -->', replace = "" },
{ pattern = '>---+\n', replace = '' },
{ pattern = ' +\n', replace = "\n" },
]
[git]
# parse the commits based on https://www.conventionalcommits.org
@@ -68,7 +92,7 @@ filter_unconventional = true
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/ratatui-org/ratatui/issues/${2}))" },
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
{ pattern = '(better safe shared layout cache)', replace = "perf(layout): ${1}" },
{ pattern = '(Clarify README.md)', replace = "docs(readme): ${1}" },
{ pattern = '(Update README.md)', replace = "docs(readme): ${1}" },
@@ -97,6 +121,7 @@ commit_parsers = [
{ message = "^(Buffer|buffer|Frame|frame|Gauge|gauge|Paragraph|paragraph):", group = "<!-- 07 -->Miscellaneous Tasks" },
{ message = "^\\[", group = "<!-- 07 -->Miscellaneous Tasks" },
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers

24
clippy.toml Normal file
View File

@@ -0,0 +1,24 @@
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
# (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",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"unicode-width",
]

View File

@@ -1,9 +1,7 @@
# configuration for https://github.com/EmbarkStudios/cargo-deny
[licenses]
default = "deny"
unlicensed = "deny"
copyleft = "deny"
version = 2
confidence-threshold = 0.8
allow = [
"Apache-2.0",
@@ -13,11 +11,11 @@ allow = [
"MIT",
"Unicode-DFS-2016",
"WTFPL",
"Zlib",
]
[advisories]
unmaintained = "deny"
yanked = "deny"
version = 2
[bans]
multiple-versions = "allow"

View File

@@ -1,292 +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 std::{
error::Error,
io,
time::{Duration, Instant},
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
struct Company<'a> {
revenue: [u64; 4],
label: &'a str,
bar_style: Style,
}
struct App<'a> {
data: Vec<(&'a str, u64)>,
months: [&'a str; 4],
companies: [Company<'a>; 3],
}
const TOTAL_REVENUE: &str = "Total Revenue";
impl<'a> App<'a> {
fn new() -> App<'a> {
App {
data: vec![
("B1", 9),
("B2", 12),
("B3", 5),
("B4", 8),
("B5", 2),
("B6", 4),
("B7", 5),
("B8", 9),
("B9", 14),
("B10", 15),
("B11", 1),
("B12", 0),
("B13", 4),
("B14", 6),
("B15", 4),
("B16", 6),
("B17", 4),
("B18", 7),
("B19", 13),
("B20", 8),
("B21", 11),
("B22", 9),
("B23", 3),
("B24", 5),
],
companies: [
Company {
label: "Comp.A",
revenue: [9500, 12500, 5300, 8500],
bar_style: Style::default().fg(Color::Green),
},
Company {
label: "Comp.B",
revenue: [1500, 2500, 3000, 500],
bar_style: Style::default().fg(Color::Yellow),
},
Company {
label: "Comp.C",
revenue: [10500, 10600, 9000, 4200],
bar_style: Style::default().fg(Color::White),
},
],
months: ["Mars", "Apr", "May", "Jun"],
}
}
fn on_tick(&mut self) {
let value = self.data.pop().unwrap();
self.data.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 let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui(frame: &mut Frame, app: &App) {
let vertical = Layout::vertical([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]);
let horizontal = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [top, bottom] = vertical.areas(frame.size());
let [left, right] = horizontal.areas(bottom);
let barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
.data(&app.data)
.bar_width(9)
.bar_style(Style::default().fg(Color::Yellow))
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
frame.render_widget(barchart, top);
draw_bar_with_group_labels(frame, app, left);
draw_horizontal_bars(frame, app, right);
}
fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGroup<'a>> {
app.months
.iter()
.enumerate()
.map(|(i, &month)| {
let bars: Vec<Bar> = app
.companies
.iter()
.map(|c| {
let mut bar = Bar::default()
.value(c.revenue[i])
.style(c.bar_style)
.value_style(
Style::default()
.bg(c.bar_style.fg.unwrap())
.fg(Color::Black),
);
if combine_values_and_labels {
bar = bar.text_value(format!(
"{} ({:.1} M)",
c.label,
(c.revenue[i] as f64) / 1000.
));
} else {
bar = bar
.text_value(format!("{:.1}", (c.revenue[i] as f64) / 1000.))
.label(c.label.into());
}
bar
})
.collect();
BarGroup::default()
.label(Line::from(month).centered())
.bars(&bars)
})
.collect()
}
fn draw_bar_with_group_labels(f: &mut Frame, app: &App, area: Rect) {
let groups = create_groups(app, false);
let mut barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
.bar_width(7)
.group_gap(3);
for group in groups {
barchart = barchart.data(group)
}
f.render_widget(barchart, area);
const LEGEND_HEIGHT: u16 = 6;
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
let legend_width = TOTAL_REVENUE.len() as u16 + 2;
let legend_area = Rect {
height: LEGEND_HEIGHT,
width: legend_width,
y: area.y,
x: area.right() - legend_width,
};
draw_legend(f, legend_area);
}
}
fn draw_horizontal_bars(f: &mut Frame, app: &App, area: Rect) {
let groups = create_groups(app, true);
let mut barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
.bar_width(1)
.group_gap(1)
.bar_gap(0)
.direction(Direction::Horizontal);
for group in groups {
barchart = barchart.data(group)
}
f.render_widget(barchart, area);
const LEGEND_HEIGHT: u16 = 6;
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
let legend_width = TOTAL_REVENUE.len() as u16 + 2;
let legend_area = Rect {
height: LEGEND_HEIGHT,
width: legend_width,
y: area.y,
x: area.right() - legend_width,
};
draw_legend(f, legend_area);
}
}
fn draw_legend(f: &mut Frame, area: Rect) {
let text = vec![
Line::from(Span::styled(
TOTAL_REVENUE,
Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::White),
)),
Line::from(Span::styled(
"- Company A",
Style::default().fg(Color::Green),
)),
Line::from(Span::styled(
"- Company B",
Style::default().fg(Color::Yellow),
)),
Line::from(vec![Span::styled(
"- Company C",
Style::default().fg(Color::White),
)]),
];
let block = Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::White));
let paragraph = Paragraph::new(text).block(block);
f.render_widget(paragraph, area);
}

View File

@@ -1,261 +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 crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use itertools::Itertools;
use ratatui::{
prelude::*,
widgets::{
block::{Position, Title},
Block, BorderType, Borders, Padding, Paragraph, Wrap,
},
};
// 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 let KeyCode::Char('q') = key.code {
return Ok(ControlFlow::Break(()));
}
}
}
Ok(ControlFlow::Continue(()))
}
fn ui(frame: &mut Frame) {
let (title_area, layout) = calculate_layout(frame.size());
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:#?}", border = 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::new()
.borders(Borders::ALL)
.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::new()
.borders(Borders::ALL)
.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::new()
.borders(Borders::ALL)
.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::new()
.borders(Borders::ALL)
.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::new().borders(Borders::ALL).title(title);
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_multiple_titles(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
.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::new()
.borders(Borders::ALL)
.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::new()
.borders(Borders::ALL)
.title("Padding")
.padding(Padding::new(5, 10, 1, 2));
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_nested_blocks(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let outer_block = Block::new().borders(Borders::ALL).title("Outer block");
let inner_block = Block::new().borders(Borders::ALL).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,209 +0,0 @@
//! # [Ratatui] Canvas example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui-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, Stdout},
time::{Duration, Instant},
};
use crossterm::{
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{
prelude::*,
widgets::{canvas::*, *},
};
fn main() -> io::Result<()> {
App::run()
}
struct App {
x: f64,
y: f64,
ball: Circle,
playground: Rect,
vx: f64,
vy: f64,
tick_count: u64,
marker: Marker,
}
impl App {
fn new() -> App {
App {
x: 0.0,
y: 0.0,
ball: Circle {
x: 20.0,
y: 40.0,
radius: 10.0,
color: Color::Yellow,
},
playground: Rect::new(10, 10, 200, 100),
vx: 1.0,
vy: 1.0,
tick_count: 0,
marker: Marker::Dot,
}
}
pub fn run() -> io::Result<()> {
let mut terminal = init_terminal()?;
let mut app = App::new();
let mut last_tick = Instant::now();
let tick_rate = Duration::from_millis(16);
loop {
let _ = terminal.draw(|frame| app.ui(frame));
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => break,
KeyCode::Down | KeyCode::Char('j') => app.y += 1.0,
KeyCode::Up | KeyCode::Char('k') => app.y -= 1.0,
KeyCode::Right | KeyCode::Char('l') => app.x += 1.0,
KeyCode::Left | KeyCode::Char('h') => app.x -= 1.0,
_ => {}
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
restore_terminal()
}
fn on_tick(&mut self) {
self.tick_count += 1;
// only change marker every 180 ticks (3s) to avoid stroboscopic effect
if (self.tick_count % 180) == 0 {
self.marker = match self.marker {
Marker::Dot => Marker::Braille,
Marker::Braille => Marker::Block,
Marker::Block => Marker::HalfBlock,
Marker::HalfBlock => Marker::Bar,
Marker::Bar => Marker::Dot,
};
}
// bounce the ball by flipping the velocity vector
let ball = &self.ball;
let playground = self.playground;
if ball.x - ball.radius < playground.left() as f64
|| ball.x + ball.radius > playground.right() as f64
{
self.vx = -self.vx;
}
if ball.y - ball.radius < playground.top() as f64
|| ball.y + ball.radius > playground.bottom() as f64
{
self.vy = -self.vy;
}
self.ball.x += self.vx;
self.ball.y += self.vy;
}
fn ui(&self, frame: &mut Frame) {
let horizontal =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [map, right] = horizontal.areas(frame.size());
let [pong, boxes] = vertical.areas(right);
frame.render_widget(self.map_canvas(), map);
frame.render_widget(self.pong_canvas(), pong);
frame.render_widget(self.boxes_canvas(boxes), boxes);
}
fn map_canvas(&self) -> impl Widget + '_ {
Canvas::default()
.block(Block::default().borders(Borders::ALL).title("World"))
.marker(self.marker)
.paint(|ctx| {
ctx.draw(&Map {
color: Color::Green,
resolution: MapResolution::High,
});
ctx.print(self.x, -self.y, "You are here".yellow());
})
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0])
}
fn pong_canvas(&self) -> impl Widget + '_ {
Canvas::default()
.block(Block::default().borders(Borders::ALL).title("Pong"))
.marker(self.marker)
.paint(|ctx| {
ctx.draw(&self.ball);
})
.x_bounds([10.0, 210.0])
.y_bounds([10.0, 110.0])
}
fn boxes_canvas(&self, area: Rect) -> impl Widget {
let (left, right, bottom, top) =
(0.0, area.width as f64, 0.0, area.height as f64 * 2.0 - 4.0);
Canvas::default()
.block(Block::default().borders(Borders::ALL).title("Rects"))
.marker(self.marker)
.x_bounds([left, right])
.y_bounds([bottom, top])
.paint(|ctx| {
for i in 0..=11 {
ctx.draw(&Rectangle {
x: (i * i + 3 * i) as f64 / 2.0 + 2.0,
y: 2.0,
width: i as f64,
height: i as f64,
color: Color::Red,
});
ctx.draw(&Rectangle {
x: (i * i + 3 * i) as f64 / 2.0 + 2.0,
y: 21.0,
width: i as f64,
height: i as f64,
color: Color::Blue,
});
}
for i in 0..100 {
if i % 10 != 0 {
ctx.print(i as f64 + 1.0, 0.0, format!("{i}", i = i % 10));
}
if i % 2 == 0 && i % 10 != 0 {
ctx.print(0.0, i as f64, format!("{i}", i = i % 10));
}
}
})
}
}
fn init_terminal() -> io::Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
Terminal::new(CrosstermBackend::new(stdout()))
}
fn restore_terminal() -> io::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -1,368 +0,0 @@
//! # [Ratatui] Chart example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui-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 crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
prelude::*,
widgets::{block::Title, *},
};
#[derive(Clone)]
pub struct SinSignal {
x: f64,
interval: f64,
period: f64,
scale: f64,
}
impl SinSignal {
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
SinSignal {
x: 0.0,
interval,
period,
scale,
}
}
}
impl Iterator for SinSignal {
type Item = (f64, f64);
fn next(&mut self) -> Option<Self::Item> {
let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
self.x += self.interval;
Some(point)
}
}
struct App {
signal1: SinSignal,
data1: Vec<(f64, f64)>,
signal2: SinSignal,
data2: Vec<(f64, f64)>,
window: [f64; 2],
}
impl App {
fn new() -> App {
let mut signal1 = SinSignal::new(0.2, 3.0, 18.0);
let mut signal2 = SinSignal::new(0.1, 2.0, 10.0);
let data1 = signal1.by_ref().take(200).collect::<Vec<(f64, f64)>>();
let data2 = signal2.by_ref().take(200).collect::<Vec<(f64, f64)>>();
App {
signal1,
data1,
signal2,
data2,
window: [0.0, 20.0],
}
}
fn on_tick(&mut self) {
for _ in 0..5 {
self.data1.remove(0);
}
self.data1.extend(self.signal1.by_ref().take(5));
for _ in 0..10 {
self.data2.remove(0);
}
self.data2.extend(self.signal2.by_ref().take(10));
self.window[0] += 1.0;
self.window[1] += 1.0;
}
}
fn main() -> Result<(), Box<dyn Error>> {
// 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 let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui(frame: &mut Frame, app: &App) {
let area = frame.size();
let vertical = Layout::vertical([Constraint::Percentage(40), Constraint::Percentage(60)]);
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
let [chart1, bottom] = vertical.areas(area);
let [line_chart, scatter] = horizontal.areas(bottom);
render_chart1(frame, chart1, app);
render_line_chart(frame, line_chart);
render_scatter(frame, scatter);
}
fn render_chart1(f: &mut Frame, area: Rect, app: &App) {
let x_labels = vec![
Span::styled(
format!("{}", app.window[0]),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(format!("{}", (app.window[0] + app.window[1]) / 2.0)),
Span::styled(
format!("{}", app.window[1]),
Style::default().add_modifier(Modifier::BOLD),
),
];
let datasets = vec![
Dataset::default()
.name("data2")
.marker(symbols::Marker::Dot)
.style(Style::default().fg(Color::Cyan))
.data(&app.data1),
Dataset::default()
.name("data3")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.data(&app.data2),
];
let chart = Chart::new(datasets)
.block(
Block::default()
.title("Chart 1".cyan().bold())
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().fg(Color::Gray))
.labels(x_labels)
.bounds(app.window),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.labels(vec!["-20".bold(), "0".into(), "20".bold()])
.bounds([-20.0, 20.0]),
);
f.render_widget(chart, area);
}
fn render_line_chart(f: &mut Frame, area: Rect) {
let datasets = vec![Dataset::default()
.name("Line from only 2 points".italic())
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.graph_type(GraphType::Line)
.data(&[(1., 1.), (4., 4.)])];
let chart = Chart::new(datasets)
.block(
Block::default()
.title(
Title::default()
.content("Line chart".cyan().bold())
.alignment(Alignment::Center),
)
.borders(Borders::ALL),
)
.x_axis(
Axis::default()
.title("X Axis")
.style(Style::default().gray())
.bounds([0.0, 5.0])
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().gray())
.bounds([0.0, 5.0])
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
)
.legend_position(Some(LegendPosition::TopLeft))
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
f.render_widget(chart, area)
}
fn render_scatter(f: &mut Frame, area: Rect) {
let datasets = vec![
Dataset::default()
.name("Heavy")
.marker(Marker::Dot)
.graph_type(GraphType::Scatter)
.style(Style::new().yellow())
.data(&HEAVY_PAYLOAD_DATA),
Dataset::default()
.name("Medium".underlined())
.marker(Marker::Braille)
.graph_type(GraphType::Scatter)
.style(Style::new().magenta())
.data(&MEDIUM_PAYLOAD_DATA),
Dataset::default()
.name("Small")
.marker(Marker::Dot)
.graph_type(GraphType::Scatter)
.style(Style::new().cyan())
.data(&SMALL_PAYLOAD_DATA),
];
let chart = Chart::new(datasets)
.block(
Block::new().borders(Borders::all()).title(
Title::default()
.content("Scatter chart".cyan().bold())
.alignment(Alignment::Center),
),
)
.x_axis(
Axis::default()
.title("Year")
.bounds([1960., 2020.])
.style(Style::default().fg(Color::Gray))
.labels(vec!["1960".into(), "1990".into(), "2020".into()]),
)
.y_axis(
Axis::default()
.title("Cost")
.bounds([0., 75000.])
.style(Style::default().fg(Color::Gray))
.labels(vec!["0".into(), "37 500".into(), "75 000".into()]),
)
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
f.render_widget(chart, area);
}
// Data from https://ourworldindata.org/space-exploration-satellites
const HEAVY_PAYLOAD_DATA: [(f64, f64); 9] = [
(1965., 8200.),
(1967., 5400.),
(1981., 65400.),
(1989., 30800.),
(1997., 10200.),
(2004., 11600.),
(2014., 4500.),
(2016., 7900.),
(2018., 1500.),
];
const MEDIUM_PAYLOAD_DATA: [(f64, f64); 29] = [
(1963., 29500.),
(1964., 30600.),
(1965., 177900.),
(1965., 21000.),
(1966., 17900.),
(1966., 8400.),
(1975., 17500.),
(1982., 8300.),
(1985., 5100.),
(1988., 18300.),
(1990., 38800.),
(1990., 9900.),
(1991., 18700.),
(1992., 9100.),
(1994., 10500.),
(1994., 8500.),
(1994., 8700.),
(1997., 6200.),
(1999., 18000.),
(1999., 7600.),
(1999., 8900.),
(1999., 9600.),
(2000., 16000.),
(2001., 10000.),
(2002., 10400.),
(2002., 8100.),
(2010., 2600.),
(2013., 13600.),
(2017., 8000.),
];
const SMALL_PAYLOAD_DATA: [(f64, f64); 23] = [
(1961., 118500.),
(1962., 14900.),
(1975., 21400.),
(1980., 32800.),
(1988., 31100.),
(1990., 41100.),
(1993., 23600.),
(1994., 20600.),
(1994., 34600.),
(1996., 50600.),
(1997., 19200.),
(1997., 45800.),
(1998., 19100.),
(2000., 73100.),
(2003., 11200.),
(2008., 12600.),
(2010., 30500.),
(2012., 20000.),
(2013., 10600.),
(2013., 34500.),
(2015., 10600.),
(2018., 23100.),
(2019., 17300.),
];

View File

@@ -1,815 +0,0 @@
//! [tui-big-text] is a rust crate that renders large pixel text as a [Ratatui] widget using the
//! glyphs from the [font8x8] crate.
//!
//! ![Hello World example](https://vhs.charm.sh/vhs-2UxNc2SJgiNqHoowbsXAMW.gif)
//!
//! # Installation
//!
//! ```shell
//! cargo add ratatui tui-big-text
//! ```
//!
//! # Usage
//!
//! Create a [`BigText`] widget using `BigTextBuilder` and pass it to [`Frame::render_widget`] to
//! render be rendered. The builder allows you to customize the [`Style`] of the widget and the
//! [`PixelSize`] of the glyphs. The [`PixelSize`] can be used to control how many character cells
//! are used to represent a single pixel of the 8x8 font.
//!
//! # Example
//!
//! ```rust
//! use anyhow::Result;
//! use ratatui::prelude::*;
//! use tui_big_text::{BigTextBuilder, PixelSize};
//!
//! fn render(frame: &mut Frame) -> Result<()> {
//! let big_text = BigTextBuilder::default()
//! .pixel_size(PixelSize::Full)
//! .style(Style::new().blue())
//! .lines(vec![
//! "Hello".red().into(),
//! "World".white().into(),
//! "~~~~~".into(),
//! ])
//! .build()?;
//! frame.render_widget(big_text, frame.size());
//! Ok(())
//! }
//! ```
//!
//! [tui-big-text]: https://crates.io/crates/tui-big-text
//! [Ratatui]: https://crates.io/crates/ratatui
//! [font8x8]: https://crates.io/crates/font8x8
//! [`BigText`]: crate::BigText
//! [`PixelSize`]: crate::PixelSize
//! [`Frame::render_widget`]: ratatui::Frame::render_widget
//! [`Style`]: ratatui::style::Style
use std::cmp::min;
use derive_builder::Builder;
use font8x8::UnicodeFonts;
use ratatui::{prelude::*, text::StyledGrapheme, widgets::Widget};
#[allow(unused)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
pub enum PixelSize {
#[default]
/// A pixel from the 8x8 font is represented by a full character cell in the terminal.
Full,
/// A pixel from the 8x8 font is represented by a half (upper/lower) character cell in the
/// terminal.
HalfHeight,
/// A pixel from the 8x8 font is represented by a half (left/right) character cell in the
/// terminal.
HalfWidth,
/// A pixel from the 8x8 font is represented by a quadrant of a character cell in the terminal.
Quadrant,
}
/// Displays one or more lines of text using 8x8 pixel characters.
///
/// The text is rendered using the [font8x8](https://crates.io/crates/font8x8) crate.
///
/// Using the `pixel_size` method, you can also chose, how 'big' a pixel should be.
/// Currently a pixel of the 8x8 font can be represented by one full or half
/// (horizontal/vertical/both) character cell of the terminal.
///
/// # Examples
///
/// ```rust
/// use ratatui::prelude::*;
/// use tui_big_text::{BigTextBuilder, PixelSize};
///
/// BigText::builder()
/// .pixel_size(PixelSize::Full)
/// .style(Style::new().white())
/// .lines(vec![
/// "Hello".red().into(),
/// "World".blue().into(),
/// "=====".into(),
/// ])
/// .build();
/// ```
///
/// Renders:
///
/// ```plain
/// ██ ██ ███ ███
/// ██ ██ ██ ██
/// ██ ██ ████ ██ ██ ████
/// ██████ ██ ██ ██ ██ ██ ██
/// ██ ██ ██████ ██ ██ ██ ██
/// ██ ██ ██ ██ ██ ██ ██
/// ██ ██ ████ ████ ████ ████
///
/// ██ ██ ███ ███
/// ██ ██ ██ ██
/// ██ ██ ████ ██ ███ ██ ██
/// ██ █ ██ ██ ██ ███ ██ ██ █████
/// ███████ ██ ██ ██ ██ ██ ██ ██
/// ███ ███ ██ ██ ██ ██ ██ ██
/// ██ ██ ████ ████ ████ ███ ██
///
/// ███ ██ ███ ██ ███ ██ ███ ██ ███ ██
/// ██ ███ ██ ███ ██ ███ ██ ███ ██ ███
/// ```
#[derive(Debug, Builder, Clone, PartialEq, Eq, Hash)]
pub struct BigText<'a> {
/// The text to display
#[builder(setter(into))]
lines: Vec<Line<'a>>,
/// The style of the widget
///
/// Defaults to `Style::default()`
#[builder(default)]
style: Style,
/// The size of single glyphs
///
/// Defaults to `BigTextSize::default()` (=> BigTextSize::Full)
#[builder(default)]
pixel_size: PixelSize,
}
impl Widget for BigText<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let layout = layout(area, &self.pixel_size);
for (line, line_layout) in self.lines.iter().zip(layout) {
for (g, cell) in line.styled_graphemes(self.style).zip(line_layout) {
render_symbol(g, cell, buf, &self.pixel_size);
}
}
}
}
/// Returns how many cells are needed to display a full 8x8 glyphe using the given font size
fn cells_per_glyph(size: &PixelSize) -> (u16, u16) {
match size {
PixelSize::Full => (8, 8),
PixelSize::HalfHeight => (8, 4),
PixelSize::HalfWidth => (4, 8),
PixelSize::Quadrant => (4, 4),
}
}
/// Chunk the area into as many x*y cells as possible returned as a 2D iterator of `Rect`s
/// representing the rows of cells.
/// The size of each cell depends on given font size
fn layout(
area: Rect,
pixel_size: &PixelSize,
) -> impl IntoIterator<Item = impl IntoIterator<Item = Rect>> {
let (width, height) = cells_per_glyph(pixel_size);
(area.top()..area.bottom())
.step_by(height as usize)
.map(move |y| {
(area.left()..area.right())
.step_by(width as usize)
.map(move |x| {
let width = min(area.right() - x, width);
let height = min(area.bottom() - y, height);
Rect::new(x, y, width, height)
})
})
}
/// Render a single grapheme into a cell by looking up the corresponding 8x8 bitmap in the
/// `BITMAPS` array and setting the corresponding cells in the buffer.
fn render_symbol(grapheme: StyledGrapheme, area: Rect, buf: &mut Buffer, pixel_size: &PixelSize) {
buf.set_style(area, grapheme.style);
let c = grapheme.symbol.chars().next().unwrap(); // TODO: handle multi-char graphemes
if let Some(glyph) = font8x8::BASIC_FONTS.get(c) {
render_glyph(glyph, area, buf, pixel_size);
}
}
/// Get the correct unicode symbol for two vertical "pixels"
fn get_symbol_half_height(top: u8, bottom: u8) -> char {
match top {
0 => match bottom {
0 => ' ',
_ => '▄',
},
_ => match bottom {
0 => '▀',
_ => '█',
},
}
}
/// Get the correct unicode symbol for two horizontal "pixels"
fn get_symbol_half_width(left: u8, right: u8) -> char {
match left {
0 => match right {
0 => ' ',
_ => '▐',
},
_ => match right {
0 => '▌',
_ => '█',
},
}
}
/// Get the correct unicode symbol for 2x2 "pixels"
fn get_symbol_half_size(top_left: u8, top_right: u8, bottom_left: u8, bottom_right: u8) -> char {
let top_left = if top_left > 0 { 1 } else { 0 };
let top_right = if top_right > 0 { 1 } else { 0 };
let bottom_left = if bottom_left > 0 { 1 } else { 0 };
let bottom_right = if bottom_right > 0 { 1 } else { 0 };
const QUADRANT_SYMBOLS: [char; 16] = [
' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛', '▗', '▚', '▐', '▜', '▄', '▙', '▟', '█',
];
QUADRANT_SYMBOLS[top_left + (top_right << 1) + (bottom_left << 2) + (bottom_right << 3)]
}
/// Render a single 8x8 glyph into a cell by setting the corresponding cells in the buffer.
fn render_glyph(glyph: [u8; 8], area: Rect, buf: &mut Buffer, pixel_size: &PixelSize) {
let (width, height) = cells_per_glyph(pixel_size);
let glyph_vertical_index = (0..glyph.len()).step_by(8 / height as usize);
let glyph_horizontal_bit_selector = (0..8).step_by(8 / width as usize);
for (row, y) in glyph_vertical_index.zip(area.top()..area.bottom()) {
for (col, x) in glyph_horizontal_bit_selector
.clone()
.zip(area.left()..area.right())
{
let cell = buf.get_mut(x, y);
let symbol_character = match pixel_size {
PixelSize::Full => match glyph[row] & (1 << col) {
0 => ' ',
_ => '█',
},
PixelSize::HalfHeight => {
let top = glyph[row] & (1 << col);
let bottom = glyph[row + 1] & (1 << col);
get_symbol_half_height(top, bottom)
}
PixelSize::HalfWidth => {
let left = glyph[row] & (1 << col);
let right = glyph[row] & (1 << (col + 1));
get_symbol_half_width(left, right)
}
PixelSize::Quadrant => {
let top_left = glyph[row] & (1 << col);
let top_right = glyph[row] & (1 << (col + 1));
let bottom_left = glyph[row + 1] & (1 << col);
let bottom_right = glyph[row + 1] & (1 << (col + 1));
get_symbol_half_size(top_left, top_right, bottom_left, bottom_right)
}
};
cell.set_char(symbol_character);
}
}
}
#[cfg(test)]
mod tests {
use ratatui::assert_buffer_eq;
use super::*;
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
#[test]
fn build() -> Result<()> {
let lines = vec![Line::from(vec!["Hello".red(), "World".blue()])];
let style = Style::new().green();
let pixel_size = PixelSize::default();
assert_eq!(
BigTextBuilder::default()
.lines(lines.clone())
.style(style)
.build()?,
BigText {
lines,
style,
pixel_size
}
);
Ok(())
}
#[test]
fn render_single_line() -> Result<()> {
let big_text = BigTextBuilder::default()
.lines(vec![Line::from("SingleLine")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 80, 8));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
" ████ ██ ███ ████ ██ ",
"██ ██ ██ ██ ",
"███ ███ █████ ███ ██ ██ ████ ██ ███ █████ ████ ",
" ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ",
" ███ ██ ██ ██ ██ ██ ██ ██████ ██ █ ██ ██ ██ ██████ ",
"██ ██ ██ ██ ██ █████ ██ ██ ██ ██ ██ ██ ██ ██ ",
" ████ ████ ██ ██ ██ ████ ████ ███████ ████ ██ ██ ████ ",
" █████ ",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_truncated() -> Result<()> {
let big_text = BigTextBuilder::default()
.lines(vec![Line::from("Truncated")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 70, 6));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
"██████ █ ███",
"█ ██ █ ██ ██",
" ██ ██ ███ ██ ██ █████ ████ ████ █████ ████ ██",
" ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █████",
" ██ ██ ██ ██ ██ ██ ██ ██ █████ ██ ██████ ██ ██",
" ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █ ██ ██ ██",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_multiple_lines() -> Result<()> {
let big_text = BigTextBuilder::default()
.lines(vec![Line::from("Multi"), Line::from("Lines")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 16));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
"██ ██ ███ █ ██ ",
"███ ███ ██ ██ ",
"███████ ██ ██ ██ █████ ███ ",
"███████ ██ ██ ██ ██ ██ ",
"██ █ ██ ██ ██ ██ ██ ██ ",
"██ ██ ██ ██ ██ ██ █ ██ ",
"██ ██ ███ ██ ████ ██ ████ ",
" ",
"████ ██ ",
" ██ ",
" ██ ███ █████ ████ █████ ",
" ██ ██ ██ ██ ██ ██ ██ ",
" ██ █ ██ ██ ██ ██████ ████ ",
" ██ ██ ██ ██ ██ ██ ██ ",
"███████ ████ ██ ██ ████ █████ ",
" ",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_widget_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.lines(vec![Line::from("Styled")])
.style(Style::new().bold())
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 48, 8));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
" ████ █ ███ ███ ",
"██ ██ ██ ██ ██ ",
"███ █████ ██ ██ ██ ████ ██ ",
" ███ ██ ██ ██ ██ ██ ██ █████ ",
" ███ ██ ██ ██ ██ ██████ ██ ██ ",
"██ ██ ██ █ █████ ██ ██ ██ ██ ",
" ████ ██ ██ ████ ████ ███ ██ ",
" █████ ",
]);
expected.set_style(Rect::new(0, 0, 48, 8), Style::new().bold());
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_line_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.lines(vec![
Line::from("Red".red()),
Line::from("Green".green()),
Line::from("Blue".blue()),
])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 24));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
"██████ ███ ",
" ██ ██ ██ ",
" ██ ██ ████ ██ ",
" █████ ██ ██ █████ ",
" ██ ██ ██████ ██ ██ ",
" ██ ██ ██ ██ ██ ",
"███ ██ ████ ███ ██ ",
" ",
" ████ ",
" ██ ██ ",
"██ ██ ███ ████ ████ █████ ",
"██ ███ ██ ██ ██ ██ ██ ██ ██ ",
"██ ███ ██ ██ ██████ ██████ ██ ██ ",
" ██ ██ ██ ██ ██ ██ ██ ",
" █████ ████ ████ ████ ██ ██ ",
" ",
"██████ ███ ",
" ██ ██ ██ ",
" ██ ██ ██ ██ ██ ████ ",
" █████ ██ ██ ██ ██ ██ ",
" ██ ██ ██ ██ ██ ██████ ",
" ██ ██ ██ ██ ██ ██ ",
"██████ ████ ███ ██ ████ ",
" ",
]);
expected.set_style(Rect::new(0, 0, 24, 8), Style::new().red());
expected.set_style(Rect::new(0, 8, 40, 8), Style::new().green());
expected.set_style(Rect::new(0, 16, 32, 8), Style::new().blue());
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_height_single_line() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfHeight)
.lines(vec![Line::from("SingleLine")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 80, 4));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
"▄█▀▀█▄ ▀▀ ▀██ ▀██▀ ▀▀ ",
"▀██▄ ▀██ ██▀▀█▄ ▄█▀▀▄█▀ ██ ▄█▀▀█▄ ██ ▀██ ██▀▀█▄ ▄█▀▀█▄ ",
"▄▄ ▀██ ██ ██ ██ ▀█▄▄██ ██ ██▀▀▀▀ ██ ▄█ ██ ██ ██ ██▀▀▀▀ ",
" ▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_height_truncated() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfHeight)
.lines(vec![Line::from("Truncated")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 70, 3));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
"█▀██▀█ ▄█ ▀██",
" ██ ▀█▄█▀█▄ ██ ██ ██▀▀█▄ ▄█▀▀█▄ ▀▀▀█▄ ▀██▀▀ ▄█▀▀█▄ ▄▄▄██",
" ██ ██ ▀▀ ██ ██ ██ ██ ██ ▄▄ ▄█▀▀██ ██ ▄ ██▀▀▀▀ ██ ██",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_height_multiple_lines() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfHeight)
.lines(vec![Line::from("Multi"), Line::from("Lines")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 8));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
"██▄ ▄██ ▀██ ▄█ ▀▀ ",
"███████ ██ ██ ██ ▀██▀▀ ▀██ ",
"██ ▀ ██ ██ ██ ██ ██ ▄ ██ ",
"▀▀ ▀▀ ▀▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀▀▀ ",
"▀██▀ ▀▀ ",
" ██ ▀██ ██▀▀█▄ ▄█▀▀█▄ ▄█▀▀▀▀ ",
" ██ ▄█ ██ ██ ██ ██▀▀▀▀ ▀▀▀█▄ ",
"▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀▀▀▀ ",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_height_widget_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfHeight)
.lines(vec![Line::from("Styled")])
.style(Style::new().bold())
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 48, 4));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
"▄█▀▀█▄ ▄█ ▀██ ▀██ ",
"▀██▄ ▀██▀▀ ██ ██ ██ ▄█▀▀█▄ ▄▄▄██ ",
"▄▄ ▀██ ██ ▄ ▀█▄▄██ ██ ██▀▀▀▀ ██ ██ ",
" ▀▀▀▀ ▀▀ ▄▄▄▄█▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ",
]);
expected.set_style(Rect::new(0, 0, 48, 4), Style::new().bold());
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_height_line_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfHeight)
.lines(vec![
Line::from("Red".red()),
Line::from("Green".green()),
Line::from("Blue".blue()),
])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 12));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
"▀██▀▀█▄ ▀██ ",
" ██▄▄█▀ ▄█▀▀█▄ ▄▄▄██ ",
" ██ ▀█▄ ██▀▀▀▀ ██ ██ ",
"▀▀▀ ▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ",
" ▄█▀▀█▄ ",
"██ ▀█▄█▀█▄ ▄█▀▀█▄ ▄█▀▀█▄ ██▀▀█▄ ",
"▀█▄ ▀██ ██ ▀▀ ██▀▀▀▀ ██▀▀▀▀ ██ ██ ",
" ▀▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀ ▀▀ ",
"▀██▀▀█▄ ▀██ ",
" ██▄▄█▀ ██ ██ ██ ▄█▀▀█▄ ",
" ██ ██ ██ ██ ██ ██▀▀▀▀ ",
"▀▀▀▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀ ▀▀▀▀ ",
]);
expected.set_style(Rect::new(0, 0, 24, 4), Style::new().red());
expected.set_style(Rect::new(0, 4, 40, 4), Style::new().green());
expected.set_style(Rect::new(0, 8, 32, 4), Style::new().blue());
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_width_single_line() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfWidth)
.lines(vec![Line::from("SingleLine")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 8));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
"▐█▌ █ ▐█ ██ █ ",
"█ █ █ ▐▌ ",
"█▌ ▐█ ██▌ ▐█▐▌ █ ▐█▌ ▐▌ ▐█ ██▌ ▐█▌ ",
"▐█ █ █ █ █ █ █ █ █ ▐▌ █ █ █ █ █ ",
" ▐█ █ █ █ █ █ █ ███ ▐▌ ▌ █ █ █ ███ ",
"█ █ █ █ █ ▐██ █ █ ▐▌▐▌ █ █ █ █ ",
"▐█▌ ▐█▌ █ █ █ ▐█▌ ▐█▌ ███▌▐█▌ █ █ ▐█▌ ",
" ██▌ ",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_width_truncated() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfWidth)
.lines(vec![Line::from("Truncated")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 35, 6));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
"███ ▐ ▐█",
"▌█▐ █ █",
" █ █▐█ █ █ ██▌ ▐█▌ ▐█▌ ▐██ ▐█▌ █",
" █ ▐█▐▌█ █ █ █ █ █ █ █ █ █ ▐██",
" █ ▐▌▐▌█ █ █ █ █ ▐██ █ ███ █ █",
" █ ▐▌ █ █ █ █ █ █ █ █ █▐ █ █ █",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_width_multiple_lines() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfWidth)
.lines(vec![Line::from("Multi"), Line::from("Lines")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 16));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
"█ ▐▌ ▐█ ▐ █ ",
"█▌█▌ █ █ ",
"███▌█ █ █ ▐██ ▐█ ",
"███▌█ █ █ █ █ ",
"█▐▐▌█ █ █ █ █ ",
"█ ▐▌█ █ █ █▐ █ ",
"█ ▐▌▐█▐▌▐█▌ ▐▌ ▐█▌ ",
" ",
"██ █ ",
"▐▌ ",
"▐▌ ▐█ ██▌ ▐█▌ ▐██ ",
"▐▌ █ █ █ █ █ █ ",
"▐▌ ▌ █ █ █ ███ ▐█▌ ",
"▐▌▐▌ █ █ █ █ █ ",
"███▌▐█▌ █ █ ▐█▌ ██▌ ",
" ",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_width_widget_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfWidth)
.lines(vec![Line::from("Styled")])
.style(Style::new().bold())
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 24, 8));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
"▐█▌ ▐ ▐█ ▐█ ",
"█ █ █ █ █ ",
"█▌ ▐██ █ █ █ ▐█▌ █ ",
"▐█ █ █ █ █ █ █ ▐██ ",
" ▐█ █ █ █ █ ███ █ █ ",
"█ █ █▐ ▐██ █ █ █ █ ",
"▐█▌ ▐▌ █ ▐█▌ ▐█▌ ▐█▐▌",
" ██▌ ",
]);
expected.set_style(Rect::new(0, 0, 24, 8), Style::new().bold());
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_width_line_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::HalfWidth)
.lines(vec![
Line::from("Red".red()),
Line::from("Green".green()),
Line::from("Blue".blue()),
])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 24));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
"███ ▐█ ",
"▐▌▐▌ █ ",
"▐▌▐▌▐█▌ █ ",
"▐██ █ █ ▐██ ",
"▐▌█ ███ █ █ ",
"▐▌▐▌█ █ █ ",
"█▌▐▌▐█▌ ▐█▐▌ ",
" ",
" ██ ",
"▐▌▐▌ ",
"█ █▐█ ▐█▌ ▐█▌ ██▌ ",
"█ ▐█▐▌█ █ █ █ █ █ ",
"█ █▌▐▌▐▌███ ███ █ █ ",
"▐▌▐▌▐▌ █ █ █ █ ",
" ██▌██ ▐█▌ ▐█▌ █ █ ",
" ",
"███ ▐█ ",
"▐▌▐▌ █ ",
"▐▌▐▌ █ █ █ ▐█▌ ",
"▐██ █ █ █ █ █ ",
"▐▌▐▌ █ █ █ ███ ",
"▐▌▐▌ █ █ █ █ ",
"███ ▐█▌ ▐█▐▌▐█▌ ",
" ",
]);
expected.set_style(Rect::new(0, 0, 12, 8), Style::new().red());
expected.set_style(Rect::new(0, 8, 20, 8), Style::new().green());
expected.set_style(Rect::new(0, 16, 16, 8), Style::new().blue());
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn check_half_size_symbols() -> Result<()> {
assert_eq!(get_symbol_half_size(0, 0, 0, 0), ' ');
assert_eq!(get_symbol_half_size(1, 0, 0, 0), '▘');
assert_eq!(get_symbol_half_size(0, 1, 0, 0), '▝');
assert_eq!(get_symbol_half_size(1, 1, 0, 0), '▀');
assert_eq!(get_symbol_half_size(0, 0, 1, 0), '▖');
assert_eq!(get_symbol_half_size(1, 0, 1, 0), '▌');
assert_eq!(get_symbol_half_size(0, 1, 1, 0), '▞');
assert_eq!(get_symbol_half_size(1, 1, 1, 0), '▛');
assert_eq!(get_symbol_half_size(0, 0, 0, 1), '▗');
assert_eq!(get_symbol_half_size(1, 0, 0, 1), '▚');
assert_eq!(get_symbol_half_size(0, 1, 0, 1), '▐');
assert_eq!(get_symbol_half_size(1, 1, 0, 1), '▜');
assert_eq!(get_symbol_half_size(0, 0, 1, 1), '▄');
assert_eq!(get_symbol_half_size(1, 0, 1, 1), '▙');
assert_eq!(get_symbol_half_size(0, 1, 1, 1), '▟');
assert_eq!(get_symbol_half_size(1, 1, 1, 1), '█');
Ok(())
}
#[test]
fn render_half_size_single_line() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::Quadrant)
.lines(vec![Line::from("SingleLine")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 4));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
"▟▀▙ ▀ ▝█ ▜▛ ▀ ",
"▜▙ ▝█ █▀▙ ▟▀▟▘ █ ▟▀▙ ▐▌ ▝█ █▀▙ ▟▀▙ ",
"▄▝█ █ █ █ ▜▄█ █ █▀▀ ▐▌▗▌ █ █ █ █▀▀ ",
"▝▀▘ ▝▀▘ ▀ ▀ ▄▄▛ ▝▀▘ ▝▀▘ ▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_size_truncated() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::Quadrant)
.lines(vec![Line::from("Truncated")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 35, 3));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
"▛█▜ ▟ ▝█",
" █ ▜▟▜▖█ █ █▀▙ ▟▀▙ ▝▀▙ ▝█▀ ▟▀▙ ▗▄█",
" █ ▐▌▝▘█ █ █ █ █ ▄ ▟▀█ █▗ █▀▀ █ █",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_size_multiple_lines() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::Quadrant)
.lines(vec![Line::from("Multi"), Line::from("Lines")])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 8));
big_text.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![
"█▖▟▌ ▝█ ▟ ▀ ",
"███▌█ █ █ ▝█▀ ▝█ ",
"█▝▐▌█ █ █ █▗ █ ",
"▀ ▝▘▝▀▝▘▝▀▘ ▝▘ ▝▀▘ ",
"▜▛ ▀ ",
"▐▌ ▝█ █▀▙ ▟▀▙ ▟▀▀ ",
"▐▌▗▌ █ █ █ █▀▀ ▝▀▙ ",
"▀▀▀▘▝▀▘ ▀ ▀ ▝▀▘ ▀▀▘ ",
]);
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_size_widget_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::Quadrant)
.lines(vec![Line::from("Styled")])
.style(Style::new().bold())
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 24, 4));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
"▟▀▙ ▟ ▝█ ▝█ ",
"▜▙ ▝█▀ █ █ █ ▟▀▙ ▗▄█ ",
"▄▝█ █▗ ▜▄█ █ █▀▀ █ █ ",
"▝▀▘ ▝▘ ▄▄▛ ▝▀▘ ▝▀▘ ▝▀▝▘",
]);
expected.set_style(Rect::new(0, 0, 24, 4), Style::new().bold());
assert_buffer_eq!(buf, expected);
Ok(())
}
#[test]
fn render_half_size_line_style() -> Result<()> {
let big_text = BigTextBuilder::default()
.pixel_size(PixelSize::Quadrant)
.lines(vec![
Line::from("Red".red()),
Line::from("Green".green()),
Line::from("Blue".blue()),
])
.build()?;
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 12));
big_text.render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![
"▜▛▜▖ ▝█ ",
"▐▙▟▘▟▀▙ ▗▄█ ",
"▐▌▜▖█▀▀ █ █ ",
"▀▘▝▘▝▀▘ ▝▀▝▘ ",
"▗▛▜▖ ",
"█ ▜▟▜▖▟▀▙ ▟▀▙ █▀▙ ",
"▜▖▜▌▐▌▝▘█▀▀ █▀▀ █ █ ",
" ▀▀▘▀▀ ▝▀▘ ▝▀▘ ▀ ▀ ",
"▜▛▜▖▝█ ",
"▐▙▟▘ █ █ █ ▟▀▙ ",
"▐▌▐▌ █ █ █ █▀▀ ",
"▀▀▀ ▝▀▘ ▝▀▝▘▝▀▘ ",
]);
expected.set_style(Rect::new(0, 0, 12, 4), Style::new().red());
expected.set_style(Rect::new(0, 4, 20, 4), Style::new().green());
expected.set_style(Rect::new(0, 8, 16, 4), Style::new().blue());
assert_buffer_eq!(buf, expected);
Ok(())
}
}

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,37 +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
mod app;
mod big_text;
mod colors;
mod destroy;
mod errors;
mod tabs;
mod term;
mod theme;
pub use app::*;
use color_eyre::Result;
pub use colors::*;
pub use term::*;
pub use theme::*;
fn main() -> Result<()> {
errors::init_hooks()?;
let terminal = &mut term::init()?;
App::new().run(terminal)?;
term::restore()?;
Ok(())
}

View File

@@ -1,42 +0,0 @@
use std::{
io::{self, stdout},
time::Duration,
};
use color_eyre::{eyre::WrapErr, Result};
use crossterm::{
event::{self, Event},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::prelude::*;
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,95 +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 anyhow::{Context, Result};
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
/// 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.
///
/// A more robust application would probably want to handle errors and ensure that the terminal is
/// restored to a sane state before exiting. This example does not do that. It also does not handle
/// events or update the application state. It just draws a greeting and exits when the user
/// presses 'q'.
fn main() -> Result<()> {
let mut terminal = setup_terminal().context("setup failed")?;
run(&mut terminal).context("app loop failed")?;
restore_terminal(&mut terminal).context("restore terminal failed")?;
Ok(())
}
/// 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. A more robust application would probably
/// want to handle errors and ensure that the terminal is restored to a sane state before exiting.
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
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(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
disable_raw_mode().context("failed to disable raw mode")?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)
.context("unable to switch to main screen")?;
terminal.show_cursor().context("unable to show cursor")
}
/// 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(crate::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.size());
}
/// 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.
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,362 +0,0 @@
//! # [Ratatui] List example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui-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, io::stdout};
use color_eyre::config::HookBuilder;
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{prelude::*, style::palette::tailwind, widgets::*};
const TODO_HEADER_BG: Color = tailwind::BLUE.c950;
const NORMAL_ROW_COLOR: Color = tailwind::SLATE.c950;
const ALT_ROW_COLOR: Color = tailwind::SLATE.c900;
const SELECTED_STYLE_FG: Color = tailwind::BLUE.c300;
const TEXT_COLOR: Color = tailwind::SLATE.c200;
const COMPLETED_TEXT_COLOR: Color = tailwind::GREEN.c500;
#[derive(Copy, Clone)]
enum Status {
Todo,
Completed,
}
struct TodoItem<'a> {
todo: &'a str,
info: &'a str,
status: Status,
}
struct StatefulList<'a> {
state: ListState,
items: Vec<TodoItem<'a>>,
last_selected: Option<usize>,
}
/// This struct holds the current state of the app. In particular, it has the `items` field which is
/// a wrapper around `ListState`. Keeping track of the items state let us render the associated
/// widget with its state and have access to features such as natural scrolling.
///
/// Check the event handling at the bottom to see how to change the state on incoming events.
/// Check the drawing logic for items on how to specify the highlighting style for selected items.
struct App<'a> {
items: StatefulList<'a>,
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
init_error_hooks()?;
let terminal = init_terminal()?;
// create app and run it
App::new().run(terminal)?;
restore_terminal()?;
Ok(())
}
fn init_error_hooks() -> color_eyre::Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info)
}));
Ok(())
}
fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal() -> color_eyre::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
impl App<'_> {
fn new<'a>() -> App<'a> {
App {
items: StatefulList::with_items([
("Rewrite everything with Rust!", "I can't hold my inner voice. He tells me to rewrite the complete universe with Rust", Status::Todo),
("Rewrite all of your tui apps with Ratatui", "Yes, you heard that right. Go and replace your tui with Ratatui.", Status::Completed),
("Pet your cat", "Minnak loves to be pet by you! Don't forget to pet and give some treats!", Status::Todo),
("Walk with your dog", "Max is bored, go walk with him!", Status::Todo),
("Pay the bills", "Pay the train subscription!!!", Status::Completed),
("Refactor list example", "If you see this info that means I completed this task!", Status::Completed),
]),
}
}
/// Changes the status of the selected list item
fn change_status(&mut self) {
if let Some(i) = self.items.state.selected() {
self.items.items[i].status = match self.items.items[i].status {
Status::Completed => Status::Todo,
Status::Todo => Status::Completed,
}
}
}
fn go_top(&mut self) {
self.items.state.select(Some(0))
}
fn go_bottom(&mut self) {
self.items.state.select(Some(self.items.items.len() - 1))
}
}
impl App<'_> {
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> io::Result<()> {
loop {
self.draw(&mut terminal)?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
use KeyCode::*;
match key.code {
Char('q') | Esc => return Ok(()),
Char('h') | Left => self.items.unselect(),
Char('j') | Down => self.items.next(),
Char('k') | Up => self.items.previous(),
Char('l') | Right | Enter => self.change_status(),
Char('g') => self.go_top(),
Char('G') => self.go_bottom(),
_ => {}
}
}
}
}
}
fn draw(&mut self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
terminal.draw(|f| f.render_widget(self, f.size()))?;
Ok(())
}
}
impl Widget for &mut App<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
// Create a space for header, todo list and the footer.
let vertical = Layout::vertical([
Constraint::Length(2),
Constraint::Min(0),
Constraint::Length(2),
]);
let [header_area, rest_area, footer_area] = vertical.areas(area);
// Create two chunks with equal vertical screen space. One for the list and the other for
// the info block.
let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [upper_item_list_area, lower_item_list_area] = vertical.areas(rest_area);
self.render_title(header_area, buf);
self.render_todo(upper_item_list_area, buf);
self.render_info(lower_item_list_area, buf);
self.render_footer(footer_area, buf);
}
}
impl App<'_> {
fn render_title(&self, area: Rect, buf: &mut Buffer) {
Paragraph::new("Ratatui List Example")
.bold()
.centered()
.render(area, buf);
}
fn render_todo(&mut self, area: Rect, buf: &mut Buffer) {
// We create two blocks, one is for the header (outer) and the other is for list (inner).
let outer_block = Block::default()
.borders(Borders::NONE)
.fg(TEXT_COLOR)
.bg(TODO_HEADER_BG)
.title("TODO List")
.title_alignment(Alignment::Center);
let inner_block = Block::default()
.borders(Borders::NONE)
.fg(TEXT_COLOR)
.bg(NORMAL_ROW_COLOR);
// We get the inner area from outer_block. We'll use this area later to render the table.
let outer_area = area;
let inner_area = outer_block.inner(outer_area);
// We can render the header in outer_area.
outer_block.render(outer_area, buf);
// Iterate through all elements in the `items` and stylize them.
let items: Vec<ListItem> = self
.items
.items
.iter()
.enumerate()
.map(|(i, todo_item)| todo_item.to_list_item(i))
.collect();
// Create a List from all list items and highlight the currently selected one
let items = List::new(items)
.block(inner_block)
.highlight_style(
Style::default()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::REVERSED)
.fg(SELECTED_STYLE_FG),
)
.highlight_symbol(">")
.highlight_spacing(HighlightSpacing::Always);
// We can now render the item list
// (look careful we are using StatefulWidget's render.)
// ratatui::widgets::StatefulWidget::render as stateful_render
StatefulWidget::render(items, inner_area, buf, &mut self.items.state);
}
fn render_info(&self, area: Rect, buf: &mut Buffer) {
// We get the info depending on the item's state.
let info = if let Some(i) = self.items.state.selected() {
match self.items.items[i].status {
Status::Completed => "✓ DONE: ".to_string() + self.items.items[i].info,
Status::Todo => "TODO: ".to_string() + self.items.items[i].info,
}
} else {
"Nothing to see here...".to_string()
};
// We show the list item's info under the list in this paragraph
let outer_info_block = Block::default()
.borders(Borders::NONE)
.fg(TEXT_COLOR)
.bg(TODO_HEADER_BG)
.title("TODO Info")
.title_alignment(Alignment::Center);
let inner_info_block = Block::default()
.borders(Borders::NONE)
.bg(NORMAL_ROW_COLOR)
.padding(Padding::horizontal(1));
// This is a similar process to what we did for list. outer_info_area will be used for
// header inner_info_area will be used for the list info.
let outer_info_area = area;
let inner_info_area = outer_info_block.inner(outer_info_area);
// We can render the header. Inner info will be rendered later
outer_info_block.render(outer_info_area, buf);
let info_paragraph = Paragraph::new(info)
.block(inner_info_block)
.fg(TEXT_COLOR)
.wrap(Wrap { trim: false });
// We can now render the item info
info_paragraph.render(inner_info_area, buf);
}
fn render_footer(&self, area: Rect, buf: &mut Buffer) {
Paragraph::new(
"\nUse ↓↑ to move, ← to unselect, → to change status, g/G to go top/bottom.",
)
.centered()
.render(area, buf);
}
}
impl StatefulList<'_> {
fn with_items<'a>(items: [(&'a str, &'a str, Status); 6]) -> StatefulList<'a> {
StatefulList {
state: ListState::default(),
items: items.iter().map(TodoItem::from).collect(),
last_selected: None,
}
}
fn next(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i >= self.items.len() - 1 {
0
} else {
i + 1
}
}
None => self.last_selected.unwrap_or(0),
};
self.state.select(Some(i));
}
fn previous(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
self.items.len() - 1
} else {
i - 1
}
}
None => self.last_selected.unwrap_or(0),
};
self.state.select(Some(i));
}
fn unselect(&mut self) {
let offset = self.state.offset();
self.last_selected = self.state.selected();
self.state.select(None);
*self.state.offset_mut() = offset;
}
}
impl TodoItem<'_> {
fn to_list_item(&self, index: usize) -> ListItem {
let bg_color = match index % 2 {
0 => NORMAL_ROW_COLOR,
_ => ALT_ROW_COLOR,
};
let line = match self.status {
Status::Todo => Line::styled(format!("{}", self.todo), TEXT_COLOR),
Status::Completed => Line::styled(
format!("{}", self.todo),
(COMPLETED_TEXT_COLOR, bg_color),
),
};
ListItem::new(line).bg(bg_color)
}
}
impl<'a> From<&(&'a str, &'a str, Status)> for TodoItem<'a> {
fn from((todo, info, status): &(&'a str, &'a str, Status)) -> Self {
Self {
todo,
info,
status: *status,
}
}
}

View File

@@ -1,149 +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 crossterm::{
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
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 b = Block::default()
.title("Panic Handler Demo")
.borders(Borders::ALL);
let p = Paragraph::new(text).block(b).centered();
f.render_widget(p, f.size());
}

View File

@@ -1,161 +0,0 @@
//! # [Ratatui] Paragraph example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui-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 crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
struct App {
scroll: u16,
}
impl App {
fn new() -> App {
App { scroll: 0 }
}
fn on_tick(&mut self) {
self.scroll += 1;
self.scroll %= 10;
}
}
fn main() -> Result<(), Box<dyn Error>> {
// 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 let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
fn ui(f: &mut Frame, app: &App) {
let size = f.size();
// Words made "loooong" to demonstrate line breaking.
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
long_line.push('\n');
let block = Block::default().black();
f.render_widget(block, size);
let layout = Layout::vertical([Constraint::Ratio(1, 4); 4]).split(size);
let text = vec![
Line::from("This is a line "),
Line::from("This is a line ".red()),
Line::from("This is a line".on_blue()),
Line::from("This is a longer line".crossed_out()),
Line::from(long_line.on_green()),
Line::from("This is a line".green().italic()),
Line::from(vec![
"Masked text: ".into(),
Span::styled(
Masked::new("password", '*'),
Style::default().fg(Color::Red),
),
]),
];
let create_block = |title| {
Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::Gray))
.title(Span::styled(
title,
Style::default().add_modifier(Modifier::BOLD),
))
};
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Default alignment (Left), no wrap"));
f.render_widget(paragraph, layout[0]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Default alignment (Left), with wrap"))
.wrap(Wrap { trim: true });
f.render_widget(paragraph, layout[1]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Right alignment, with wrap"))
.right_aligned()
.wrap(Wrap { trim: true });
f.render_widget(paragraph, layout[2]);
let paragraph = Paragraph::new(text)
.style(Style::default().fg(Color::Gray))
.block(create_block("Center alignment, with wrap, with scroll"))
.centered()
.wrap(Wrap { trim: true })
.scroll((app.scroll, 0));
f.render_widget(paragraph, layout[3]);
}

View File

@@ -1,126 +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 crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
struct App {
show_popup: bool,
}
impl App {
fn new() -> App {
App { show_popup: false }
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let app = App::new();
let res = run_app(&mut terminal, app);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f, &app))?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => return Ok(()),
KeyCode::Char('p') => app.show_popup = !app.show_popup,
_ => {}
}
}
}
}
}
fn ui(f: &mut Frame, app: &App) {
let area = f.size();
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::default()
.title("Content")
.borders(Borders::ALL)
.on_blue();
f.render_widget(block, content);
if app.show_popup {
let block = Block::default().title("Popup").borders(Borders::ALL);
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,86 +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 crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use indoc::indoc;
use itertools::izip;
use ratatui::{prelude::*, widgets::*};
/// A fun example of using half block characters to draw a logo
fn main() -> io::Result<()> {
let r = indoc! {"
▄▄▄
█▄▄▀
█ █
"}
.lines();
let a = indoc! {"
▄▄
█▄▄█
█ █
"}
.lines();
let t = indoc! {"
▄▄▄
"}
.lines();
let u = indoc! {"
▄ ▄
█ █
▀▄▄▀
"}
.lines();
let i = indoc! {"
"}
.lines();
let mut terminal = init()?;
terminal.draw(|frame| {
let logo = izip!(r, a.clone(), t.clone(), a, t, u, i)
.map(|(r, a, t, a2, t2, u, i)| {
format!("{:5}{:5}{:4}{:5}{:4}{:5}{:5}", r, a, t, a2, t2, u, i)
})
.collect::<Vec<_>>()
.join("\n");
frame.render_widget(Paragraph::new(logo), frame.size());
})?;
sleep(Duration::from_secs(5));
restore()?;
println!();
Ok(())
}
pub fn init() -> io::Result<Terminal<impl Backend>> {
enable_raw_mode()?;
let options = TerminalOptions {
viewport: Viewport::Inline(3),
};
Terminal::with_options(CrosstermBackend::new(stdout()), options)
}
pub fn restore() -> io::Result<()> {
disable_raw_mode()?;
Ok(())
}

View File

@@ -1,237 +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
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, symbols::scrollbar, widgets::*};
#[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();
}
}
}
fn ui(f: &mut Frame, app: &mut App) {
let size = f.size();
// Words made "loooong" to demonstrate line breaking.
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
long_line.push('\n');
let chunks = Layout::vertical([
Constraint::Min(1),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
])
.split(size);
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::default().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::default().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::default()
.title("Use h j k l or ◄ ▲ ▼ ► to scroll ".bold())
.title_alignment(Alignment::Center);
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::default()
.orientation(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::default()
.orientation(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::default()
.orientation(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::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.thumb_symbol("")
.track_symbol(Some("")),
chunks[4].inner(&Margin {
vertical: 0,
horizontal: 1,
}),
&mut app.horizontal_scroll_state,
);
}

View File

@@ -1,177 +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 crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use rand::{
distributions::{Distribution, Uniform},
rngs::ThreadRng,
};
use ratatui::{prelude::*, widgets::*};
#[derive(Clone)]
pub struct RandomSignal {
distribution: Uniform<u64>,
rng: ThreadRng,
}
impl RandomSignal {
pub fn new(lower: u64, upper: u64) -> RandomSignal {
RandomSignal {
distribution: Uniform::new(lower, upper),
rng: rand::thread_rng(),
}
}
}
impl Iterator for RandomSignal {
type Item = u64;
fn next(&mut self) -> Option<u64> {
Some(self.distribution.sample(&mut self.rng))
}
}
struct App {
signal: RandomSignal,
data1: Vec<u64>,
data2: Vec<u64>,
data3: Vec<u64>,
}
impl App {
fn new() -> App {
let mut signal = RandomSignal::new(0, 100);
let data1 = signal.by_ref().take(200).collect::<Vec<u64>>();
let data2 = signal.by_ref().take(200).collect::<Vec<u64>>();
let data3 = signal.by_ref().take(200).collect::<Vec<u64>>();
App {
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 let KeyCode::Char('q') = key.code {
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.size());
let sparkline = Sparkline::default()
.block(
Block::default()
.title("Data1")
.borders(Borders::LEFT | Borders::RIGHT),
)
.data(&app.data1)
.style(Style::default().fg(Color::Yellow));
f.render_widget(sparkline, chunks[0]);
let sparkline = Sparkline::default()
.block(
Block::default()
.title("Data2")
.borders(Borders::LEFT | Borders::RIGHT),
)
.data(&app.data2)
.style(Style::default().bg(Color::Green));
f.render_widget(sparkline, chunks[1]);
// Multiline
let sparkline = Sparkline::default()
.block(
Block::default()
.title("Data3")
.borders(Borders::LEFT | Borders::RIGHT),
)
.data(&app.data3)
.style(Style::default().fg(Color::Red));
f.render_widget(sparkline, chunks[2]);
}

View File

@@ -1,367 +0,0 @@
//! # [Ratatui] Table 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};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use style::palette::tailwind;
use unicode_width::UnicodeWidthStr;
const PALETTES: [tailwind::Palette; 4] = [
tailwind::BLUE,
tailwind::EMERALD,
tailwind::INDIGO,
tailwind::RED,
];
const INFO_TEXT: &str =
"(Esc) quit | (↑) move up | (↓) move down | (→) next color | (←) previous color";
const ITEM_HEIGHT: usize = 4;
struct TableColors {
buffer_bg: Color,
header_bg: Color,
header_fg: Color,
row_fg: Color,
selected_style_fg: Color,
normal_row_color: Color,
alt_row_color: Color,
footer_border_color: Color,
}
impl TableColors {
fn new(color: &tailwind::Palette) -> Self {
Self {
buffer_bg: tailwind::SLATE.c950,
header_bg: color.c900,
header_fg: tailwind::SLATE.c200,
row_fg: tailwind::SLATE.c200,
selected_style_fg: color.c400,
normal_row_color: tailwind::SLATE.c950,
alt_row_color: tailwind::SLATE.c900,
footer_border_color: color.c400,
}
}
}
struct Data {
name: String,
address: String,
email: String,
}
impl Data {
fn ref_array(&self) -> [&String; 3] {
[&self.name, &self.address, &self.email]
}
fn name(&self) -> &str {
&self.name
}
fn address(&self) -> &str {
&self.address
}
fn email(&self) -> &str {
&self.email
}
}
struct App {
state: TableState,
items: Vec<Data>,
longest_item_lens: (u16, u16, u16), // order is (name, address, email)
scroll_state: ScrollbarState,
colors: TableColors,
color_index: usize,
}
impl App {
fn new() -> App {
let data_vec = generate_fake_names();
App {
state: TableState::default().with_selected(0),
longest_item_lens: constraint_len_calculator(&data_vec),
scroll_state: ScrollbarState::new((data_vec.len() - 1) * ITEM_HEIGHT),
colors: TableColors::new(&PALETTES[0]),
color_index: 0,
items: data_vec,
}
}
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));
self.scroll_state = self.scroll_state.position(i * ITEM_HEIGHT);
}
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));
self.scroll_state = self.scroll_state.position(i * ITEM_HEIGHT);
}
pub fn next_color(&mut self) {
self.color_index = (self.color_index + 1) % PALETTES.len();
}
pub fn previous_color(&mut self) {
let count = PALETTES.len();
self.color_index = (self.color_index + count - 1) % count;
}
pub fn set_colors(&mut self) {
self.colors = TableColors::new(&PALETTES[self.color_index])
}
}
fn generate_fake_names() -> Vec<Data> {
use fakeit::{address, contact, name};
(0..20)
.map(|_| {
let name = name::full();
let address = format!(
"{}\n{}, {} {}",
address::street(),
address::city(),
address::state(),
address::zip()
);
let email = contact::email();
Data {
name,
address,
email,
}
})
.sorted_by(|a, b| a.name.cmp(&b.name))
.collect_vec()
}
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, &mut app))?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
use KeyCode::*;
match key.code {
Char('q') | Esc => return Ok(()),
Char('j') | Down => app.next(),
Char('k') | Up => app.previous(),
Char('l') | Right => app.next_color(),
Char('h') | Left => app.previous_color(),
_ => {}
}
}
}
}
}
fn ui(f: &mut Frame, app: &mut App) {
let rects = Layout::vertical([Constraint::Min(5), Constraint::Length(3)]).split(f.size());
app.set_colors();
render_table(f, app, rects[0]);
render_scrollbar(f, app, rects[0]);
render_footer(f, app, rects[1]);
}
fn render_table(f: &mut Frame, app: &mut App, area: Rect) {
let header_style = Style::default()
.fg(app.colors.header_fg)
.bg(app.colors.header_bg);
let selected_style = Style::default()
.add_modifier(Modifier::REVERSED)
.fg(app.colors.selected_style_fg);
let header = ["Name", "Address", "Email"]
.iter()
.cloned()
.map(Cell::from)
.collect::<Row>()
.style(header_style)
.height(1);
let rows = app.items.iter().enumerate().map(|(i, data)| {
let color = match i % 2 {
0 => app.colors.normal_row_color,
_ => app.colors.alt_row_color,
};
let item = data.ref_array();
item.iter()
.cloned()
.map(|content| Cell::from(Text::from(format!("\n{}\n", content))))
.collect::<Row>()
.style(Style::new().fg(app.colors.row_fg).bg(color))
.height(4)
});
let bar = "";
let t = Table::new(
rows,
[
// + 1 is for padding.
Constraint::Length(app.longest_item_lens.0 + 1),
Constraint::Min(app.longest_item_lens.1 + 1),
Constraint::Min(app.longest_item_lens.2),
],
)
.header(header)
.highlight_style(selected_style)
.highlight_symbol(Text::from(vec![
"".into(),
bar.into(),
bar.into(),
"".into(),
]))
.bg(app.colors.buffer_bg)
.highlight_spacing(HighlightSpacing::Always);
f.render_stateful_widget(t, area, &mut app.state);
}
fn constraint_len_calculator(items: &[Data]) -> (u16, u16, u16) {
let name_len = items
.iter()
.map(Data::name)
.map(UnicodeWidthStr::width)
.max()
.unwrap_or(0);
let address_len = items
.iter()
.map(Data::address)
.flat_map(str::lines)
.map(UnicodeWidthStr::width)
.max()
.unwrap_or(0);
let email_len = items
.iter()
.map(Data::email)
.map(UnicodeWidthStr::width)
.max()
.unwrap_or(0);
(name_len as u16, address_len as u16, email_len as u16)
}
fn render_scrollbar(f: &mut Frame, app: &mut App, area: Rect) {
f.render_stateful_widget(
Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalRight)
.begin_symbol(None)
.end_symbol(None),
area.inner(&Margin {
vertical: 1,
horizontal: 1,
}),
&mut app.scroll_state,
);
}
fn render_footer(f: &mut Frame, app: &mut App, area: Rect) {
let info_footer = Paragraph::new(Line::from(INFO_TEXT))
.style(Style::new().fg(app.colors.row_fg).bg(app.colors.buffer_bg))
.centered()
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::new().fg(app.colors.footer_border_color))
.border_type(BorderType::Double),
);
f.render_widget(info_footer, area);
}
#[cfg(test)]
mod tests {
use crate::Data;
#[test]
fn constraint_len_calculator() {
let test_data = vec![
Data {
name: "Emirhan Tala".to_string(),
address: "Cambridgelaan 6XX\n3584 XX Utrecht".to_string(),
email: "tala.emirhan@gmail.com".to_string(),
},
Data {
name: "thistextis26characterslong".to_string(),
address: "this line is 31 characters long\nbottom line is 33 characters long"
.to_string(),
email: "thisemailis40caharacterslong@ratatui.com".to_string(),
},
];
let (longest_name_len, longest_address_len, longest_email_len) =
crate::constraint_len_calculator(&test_data);
assert_eq!(26, longest_name_len);
assert_eq!(33, longest_address_len);
assert_eq!(40, longest_email_len);
}
}

View File

@@ -1,260 +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
use std::{error::Error, io};
/// 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 crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
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.
cursor_position: usize,
/// Current input mode
input_mode: InputMode,
/// History of recorded messages
messages: Vec<String>,
}
impl Default for App {
fn default() -> App {
App {
input: String::new(),
input_mode: InputMode::Normal,
messages: Vec::new(),
cursor_position: 0,
}
}
}
impl App {
fn move_cursor_left(&mut self) {
let cursor_moved_left = self.cursor_position.saturating_sub(1);
self.cursor_position = self.clamp_cursor(cursor_moved_left);
}
fn move_cursor_right(&mut self) {
let cursor_moved_right = self.cursor_position.saturating_add(1);
self.cursor_position = self.clamp_cursor(cursor_moved_right);
}
fn enter_char(&mut self, new_char: char) {
self.input.insert(self.cursor_position, new_char);
self.move_cursor_right();
}
fn delete_char(&mut self) {
let is_not_cursor_leftmost = self.cursor_position != 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.cursor_position;
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.len())
}
fn reset_cursor(&mut self) {
self.cursor_position = 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::default();
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;
}
_ => {}
},
_ => {}
}
}
}
}
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.size());
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::default().borders(Borders::ALL).title("Input"));
f.render_widget(input, input_area);
match app.input_mode {
InputMode::Normal =>
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
{}
InputMode::Editing => {
// Make the cursor visible and ask ratatui to put it at the specified coordinates after
// rendering
f.set_cursor(
// 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.cursor_position 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::default().borders(Borders::ALL).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,36 +90,24 @@
//! 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};
use crate::{buffer::Cell, layout::Size, prelude::Rect};
#[cfg(feature = "termion")]
mod termion;
#[cfg(feature = "termion")]
pub use self::termion::TermionBackend;
#[cfg(feature = "crossterm")]
mod crossterm;
#[cfg(feature = "crossterm")]
pub use self::crossterm::CrosstermBackend;
#[cfg(feature = "termwiz")]
mod termwiz;
#[cfg(feature = "termwiz")]
pub use self::termwiz::TermwizBackend;
use crate::{
buffer::Cell,
layout::{Position, Size},
};
mod test;
pub use self::test::TestBackend;
@@ -159,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.
///
@@ -184,33 +172,35 @@ 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()?;
/// # std::io::Result::Ok(())
/// ```
///
/// [`show_cursor`]: Backend::show_cursor
/// [`show_cursor`]: Self::show_cursor
fn hide_cursor(&mut self) -> io::Result<()>;
/// Show the cursor on the terminal screen.
///
/// See [`hide_cursor`] for an example.
///
/// [`hide_cursor`]: Backend::hide_cursor
/// [`hide_cursor`]: Self::hide_cursor
fn show_cursor(&mut self) -> io::Result<()>;
/// Get the current cursor position on the terminal screen.
///
/// The returned tuple contains the x and y coordinates of the cursor. The origin
/// (0, 0) is at the top left corner of the screen.
/// The returned tuple contains the x and y coordinates of the cursor.
/// The origin (0, 0) is at the top left corner of the screen.
///
/// See [`set_cursor`] for an example.
/// See [`set_cursor_position`] for an example.
///
/// [`set_cursor`]: Backend::set_cursor
fn get_cursor(&mut self) -> io::Result<(u16, u16)>;
/// [`set_cursor_position`]: Self::set_cursor_position
fn get_cursor_position(&mut self) -> io::Result<Position>;
/// Set the cursor position on the terminal screen to the given x and y coordinates.
///
@@ -219,23 +209,43 @@ pub trait Backend {
/// # Example
///
/// ```rust
/// # use ratatui::backend::{Backend, TestBackend};
/// # use ratatui::backend::{TestBackend};
/// # let mut backend = TestBackend::new(80, 25);
/// backend.set_cursor(10, 20)?;
/// assert_eq!(backend.get_cursor()?, (10, 20));
/// 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(())
/// ```
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()>;
/// Get the current cursor position on the terminal screen.
///
/// [`get_cursor`]: Backend::get_cursor
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()>;
/// The returned tuple contains the x and y coordinates of the cursor. The origin
/// (0, 0) is at the top left corner of the screen.
#[deprecated = "the method get_cursor_position indicates more clearly what about the cursor to get"]
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
let Position { x, y } = self.get_cursor_position()?;
Ok((x, y))
}
/// Set the cursor position on the terminal screen to the given x and y coordinates.
///
/// The origin (0, 0) is at the top left corner of the screen.
#[deprecated = "the method set_cursor_position indicates more clearly what about the cursor to set"]
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.set_cursor_position(Position { x, y })
}
/// Clears the whole terminal screen
///
/// # 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(())
/// ```
@@ -250,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(())
/// ```
@@ -261,7 +273,7 @@ pub trait Backend {
/// This method will return an error if the terminal screen could not be cleared. It will also
/// return an error if the `clear_type` is not supported by the backend.
///
/// [`clear`]: Backend::clear
/// [`clear`]: Self::clear
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
match clear_type {
ClearType::All => self.clear(),
@@ -275,19 +287,21 @@ pub trait Backend {
}
}
/// Get the size of the terminal screen in columns/rows as a [`Rect`].
/// Get the size of the terminal screen in columns/rows as a [`Size`].
///
/// The returned [`Rect`] contains the width and height of the terminal screen.
/// The returned [`Size`] contains the width and height of the terminal screen.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::{prelude::*, backend::TestBackend};
/// let backend = TestBackend::new(80, 25);
/// assert_eq!(backend.size()?, Rect::new(0, 0, 80, 25));
/// ```rust
/// # 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(())
/// ```
fn size(&self) -> io::Result<Rect>;
fn size(&self) -> io::Result<Size>;
/// Get the size of the terminal screen in columns/rows and pixels as a [`WindowSize`].
///
@@ -298,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)]

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@
//! A module for the [`Buffer`] and [`Cell`] types.
mod assert;
#[allow(clippy::module_inception)]
mod buffer;
mod cell;

View File

@@ -0,0 +1,73 @@
/// Assert that two buffers are equal by comparing their areas and content.
///
/// # Panics
/// When the buffers differ this method panics and displays the differences similar to
/// `assert_eq!()`.
#[deprecated = "use assert_eq!(&actual, &expected)"]
#[macro_export]
macro_rules! assert_buffer_eq {
($actual_expr:expr, $expected_expr:expr) => {
match (&$actual_expr, &$expected_expr) {
(actual, expected) => {
assert!(
actual.area == expected.area,
"buffer areas not equal\nexpected: {expected:?}\nactual: {actual:?}",
);
let nice_diff = expected
.diff(actual)
.into_iter()
.enumerate()
.map(|(i, (x, y, cell))| {
let expected_cell = &expected[(x, y)];
format!("{i}: at ({x}, {y})\n expected: {expected_cell:?}\n actual: {cell:?}")
})
.collect::<Vec<String>>()
.join("\n");
assert!(
nice_diff.is_empty(),
"buffer contents not equal\nexpected: {expected:?}\nactual: {actual:?}\ndiff:\n{nice_diff}",
);
// shouldn't get here, but this guards against future behavior
// that changes equality but not area or content
assert_eq!(
actual, expected,
"buffers are not equal in an unexpected way. Please open an issue about this."
);
}
}
};
}
#[allow(deprecated)]
#[cfg(test)]
mod tests {
use crate::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
};
#[test]
fn assert_buffer_eq_does_not_panic_on_equal_buffers() {
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
let other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
assert_buffer_eq!(buffer, other_buffer);
}
#[should_panic = "buffer areas not equal"]
#[test]
fn assert_buffer_eq_panics_on_unequal_area() {
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
let other_buffer = Buffer::empty(Rect::new(0, 0, 6, 1));
assert_buffer_eq!(buffer, other_buffer);
}
#[should_panic = "buffer contents not equal"]
#[test]
fn assert_buffer_eq_panics_on_unequal_style() {
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
let mut other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
other_buffer.set_string(0, 0, " ", Style::default().fg(Color::Red));
assert_buffer_eq!(buffer, other_buffer);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,297 @@
use compact_str::CompactString;
use crate::style::{Color, Modifier, Style};
/// A buffer cell
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Cell {
/// The string to be drawn in the cell.
///
/// This accepts unicode grapheme clusters which might take up more than one 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/ratatui/pull/601> for more information.
symbol: CompactString,
/// The foreground color of the cell.
pub fg: Color,
/// The background color of the cell.
pub bg: Color,
/// The underline color of the cell.
#[cfg(feature = "underline-color")]
pub underline_color: Color,
/// The modifier of the cell.
pub modifier: Modifier,
/// Whether the cell should be skipped when copying (diffing) the buffer to the screen.
pub skip: bool,
}
impl Cell {
/// An empty `Cell`
pub const EMPTY: Self = Self::new(" ");
/// Creates a new `Cell` with the given symbol.
///
/// This works at compile time and puts the symbol onto the stack. Fails to build when the
/// symbol doesnt fit onto the stack and requires to be placed on the heap. Use
/// `Self::default().set_symbol()` in that case. See [`CompactString::const_new`] for more
/// details on this.
pub const fn new(symbol: &'static str) -> Self {
Self {
symbol: CompactString::const_new(symbol),
fg: Color::Reset,
bg: Color::Reset,
#[cfg(feature = "underline-color")]
underline_color: Color::Reset,
modifier: Modifier::empty(),
skip: false,
}
}
/// Gets the symbol of the cell.
#[must_use]
pub fn symbol(&self) -> &str {
self.symbol.as_str()
}
/// Sets the symbol of the cell.
pub fn set_symbol(&mut self, symbol: &str) -> &mut Self {
self.symbol = CompactString::new(symbol);
self
}
/// Appends a symbol to the cell.
///
/// This is particularly useful for adding zero-width characters to the cell.
pub(crate) fn append_symbol(&mut self, symbol: &str) -> &mut Self {
self.symbol.push_str(symbol);
self
}
/// Sets the symbol of the cell to a single character.
pub fn set_char(&mut self, ch: char) -> &mut Self {
let mut buf = [0; 4];
self.symbol = CompactString::new(ch.encode_utf8(&mut buf));
self
}
/// Sets the foreground color of the cell.
pub fn set_fg(&mut self, color: Color) -> &mut Self {
self.fg = color;
self
}
/// Sets the background color of the cell.
pub fn set_bg(&mut self, color: Color) -> &mut Self {
self.bg = color;
self
}
/// Sets the style of the cell.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
pub fn set_style<S: Into<Style>>(&mut self, style: S) -> &mut Self {
let style = style.into();
if let Some(c) = style.fg {
self.fg = c;
}
if let Some(c) = style.bg {
self.bg = c;
}
#[cfg(feature = "underline-color")]
if let Some(c) = style.underline_color {
self.underline_color = c;
}
self.modifier.insert(style.add_modifier);
self.modifier.remove(style.sub_modifier);
self
}
/// Returns the style of the cell.
#[must_use]
pub const fn style(&self) -> Style {
Style {
fg: Some(self.fg),
bg: Some(self.bg),
#[cfg(feature = "underline-color")]
underline_color: Some(self.underline_color),
add_modifier: self.modifier,
sub_modifier: Modifier::empty(),
}
}
/// Sets the cell to be skipped when copying (diffing) the buffer to the screen.
///
/// This is helpful when it is necessary to prevent the buffer from overwriting a cell that is
/// covered by an image from some terminal graphics protocol (Sixel / iTerm / Kitty ...).
pub fn set_skip(&mut self, skip: bool) -> &mut Self {
self.skip = skip;
self
}
/// Resets the cell to the empty state.
pub fn reset(&mut self) {
self.symbol = CompactString::const_new(" ");
self.fg = Color::Reset;
self.bg = Color::Reset;
#[cfg(feature = "underline-color")]
{
self.underline_color = Color::Reset;
}
self.modifier = Modifier::empty();
self.skip = false;
}
}
impl Default for Cell {
fn default() -> Self {
Self::EMPTY
}
}
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::*;
#[test]
fn new() {
let cell = Cell::new("");
assert_eq!(
cell,
Cell {
symbol: CompactString::const_new(""),
fg: Color::Reset,
bg: Color::Reset,
#[cfg(feature = "underline-color")]
underline_color: Color::Reset,
modifier: Modifier::empty(),
skip: false,
}
);
}
#[test]
fn empty() {
let cell = Cell::EMPTY;
assert_eq!(cell.symbol(), " ");
}
#[test]
fn set_symbol() {
let mut cell = Cell::EMPTY;
cell.set_symbol(""); // Multi-byte character
assert_eq!(cell.symbol(), "");
cell.set_symbol("👨‍👩‍👧‍👦"); // Multiple code units combined with ZWJ
assert_eq!(cell.symbol(), "👨‍👩‍👧‍👦");
}
#[test]
fn append_symbol() {
let mut cell = Cell::EMPTY;
cell.set_symbol(""); // Multi-byte character
cell.append_symbol("\u{200B}"); // zero-width space
assert_eq!(cell.symbol(), "\u{200B}");
}
#[test]
fn set_char() {
let mut cell = Cell::EMPTY;
cell.set_char('あ'); // Multi-byte character
assert_eq!(cell.symbol(), "");
}
#[test]
fn set_fg() {
let mut cell = Cell::EMPTY;
cell.set_fg(Color::Red);
assert_eq!(cell.fg, Color::Red);
}
#[test]
fn set_bg() {
let mut cell = Cell::EMPTY;
cell.set_bg(Color::Red);
assert_eq!(cell.bg, Color::Red);
}
#[test]
fn set_style() {
let mut cell = Cell::EMPTY;
cell.set_style(Style::new().fg(Color::Red).bg(Color::Blue));
assert_eq!(cell.fg, Color::Red);
assert_eq!(cell.bg, Color::Blue);
}
#[test]
fn set_skip() {
let mut cell = Cell::EMPTY;
cell.set_skip(true);
assert!(cell.skip);
}
#[test]
fn reset() {
let mut cell = Cell::EMPTY;
cell.set_symbol("");
cell.set_fg(Color::Red);
cell.set_bg(Color::Blue);
cell.set_skip(true);
cell.reset();
assert_eq!(cell.symbol(), " ");
assert_eq!(cell.fg, Color::Reset);
assert_eq!(cell.bg, Color::Reset);
assert!(!cell.skip);
}
#[test]
fn style() {
let cell = Cell::EMPTY;
assert_eq!(
cell.style(),
Style {
fg: Some(Color::Reset),
bg: Some(Color::Reset),
#[cfg(feature = "underline-color")]
underline_color: Some(Color::Reset),
add_modifier: Modifier::empty(),
sub_modifier: Modifier::empty(),
}
);
}
#[test]
fn default() {
let cell = Cell::default();
assert_eq!(cell.symbol(), " ");
}
#[test]
fn cell_eq() {
let cell1 = Cell::new("");
let cell2 = Cell::new("");
assert_eq!(cell1, cell2);
}
#[test]
fn cell_ne() {
let cell1 = Cell::new("");
let cell2 = Cell::new("");
assert_ne!(cell1, cell2);
}
}

View File

@@ -1,9 +1,10 @@
#![warn(clippy::missing_const_for_fn)]
//! Provides types and traits for working with layout and positioning in the terminal.
mod alignment;
mod constraint;
mod corner;
mod direction;
mod flex;
#[allow(clippy::module_inception)]
mod layout;
mod margin;
mod position;
@@ -12,11 +13,10 @@ mod size;
pub use alignment::Alignment;
pub use constraint::Constraint;
pub use corner::Corner;
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::{self, Display};
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
///
@@ -197,22 +201,21 @@ impl Constraint {
)]
pub fn apply(&self, length: u16) -> u16 {
match *self {
Constraint::Percentage(p) => {
let p = p as f32 / 100.0;
let length = length as f32;
Self::Percentage(p) => {
let p = f32::from(p) / 100.0;
let length = f32::from(length);
(p * length).min(length) as u16
}
Constraint::Ratio(numerator, denominator) => {
Self::Ratio(numerator, denominator) => {
// avoid division by zero by using 1 when denominator is 0
// this results in 0/0 -> 0 and x/0 -> x for x != 0
let percentage = numerator as f32 / denominator.max(1) as f32;
let length = length as f32;
let length = f32::from(length);
(percentage * length).min(length) as u16
}
Constraint::Length(l) => length.min(l),
Constraint::Fill(l) => length.min(l),
Constraint::Max(m) => length.min(m),
Constraint::Min(m) => length.max(m),
Self::Length(l) | Self::Fill(l) => length.min(l),
Self::Max(m) => length.min(m),
Self::Min(m) => length.max(m),
}
}
@@ -221,16 +224,17 @@ 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);
/// ```
pub fn from_lengths<T>(lengths: T) -> Vec<Constraint>
pub fn from_lengths<T>(lengths: T) -> Vec<Self>
where
T: IntoIterator<Item = u16>,
{
lengths.into_iter().map(Constraint::Length).collect_vec()
lengths.into_iter().map(Self::Length).collect()
}
/// Convert an iterator of ratios into a vector of constraints
@@ -238,19 +242,17 @@ 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);
/// ```
pub fn from_ratios<T>(ratios: T) -> Vec<Constraint>
pub fn from_ratios<T>(ratios: T) -> Vec<Self>
where
T: IntoIterator<Item = (u32, u32)>,
{
ratios
.into_iter()
.map(|(n, d)| Constraint::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
@@ -258,19 +260,17 @@ 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);
/// ```
pub fn from_percentages<T>(percentages: T) -> Vec<Constraint>
pub fn from_percentages<T>(percentages: T) -> Vec<Self>
where
T: IntoIterator<Item = u16>,
{
percentages
.into_iter()
.map(Constraint::Percentage)
.collect_vec()
percentages.into_iter().map(Self::Percentage).collect()
}
/// Convert an iterator of maxes into a vector of constraints
@@ -278,16 +278,17 @@ 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);
/// ```
pub fn from_maxes<T>(maxes: T) -> Vec<Constraint>
pub fn from_maxes<T>(maxes: T) -> Vec<Self>
where
T: IntoIterator<Item = u16>,
{
maxes.into_iter().map(Constraint::Max).collect_vec()
maxes.into_iter().map(Self::Max).collect()
}
/// Convert an iterator of mins into a vector of constraints
@@ -295,16 +296,17 @@ 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);
/// ```
pub fn from_mins<T>(mins: T) -> Vec<Constraint>
pub fn from_mins<T>(mins: T) -> Vec<Self>
where
T: IntoIterator<Item = u16>,
{
mins.into_iter().map(Constraint::Min).collect_vec()
mins.into_iter().map(Self::Min).collect()
}
/// Convert an iterator of proportional factors into a vector of constraints
@@ -312,69 +314,68 @@ 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 constraints = Constraint::from_fills([1, 2, 3]);
/// let layout = Layout::default().constraints(constraints).split(area);
/// ```
pub fn from_fills<T>(proportional_factors: T) -> Vec<Constraint>
pub fn from_fills<T>(proportional_factors: T) -> Vec<Self>
where
T: IntoIterator<Item = u16>,
{
proportional_factors
.into_iter()
.map(Constraint::Fill)
.collect_vec()
proportional_factors.into_iter().map(Self::Fill).collect()
}
}
impl From<u16> for Constraint {
/// Convert a u16 into a [Constraint::Length]
/// Convert a `u16` into a [`Constraint::Length`]
///
/// This is useful when you want to specify a fixed size for a layout, but don't want to
/// explicitly create a [Constraint::Length] yourself.
/// explicitly create a [`Constraint::Length`] yourself.
///
/// # 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);
/// let layout = Layout::vertical([1, 2, 3]).split(area);
/// ````
fn from(length: u16) -> Constraint {
Constraint::Length(length)
fn from(length: u16) -> Self {
Self::Length(length)
}
}
impl From<&Constraint> for Constraint {
fn from(constraint: &Constraint) -> Self {
impl From<&Self> for Constraint {
fn from(constraint: &Self) -> Self {
*constraint
}
}
impl AsRef<Constraint> for Constraint {
fn as_ref(&self) -> &Constraint {
impl AsRef<Self> for Constraint {
fn as_ref(&self) -> &Self {
self
}
}
impl Default for Constraint {
fn default() -> Self {
Constraint::Percentage(100)
Self::Percentage(100)
}
}
impl Display for Constraint {
impl fmt::Display for Constraint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Constraint::Percentage(p) => write!(f, "Percentage({})", p),
Constraint::Ratio(n, d) => write!(f, "Ratio({}, {})", n, d),
Constraint::Length(l) => write!(f, "Length({})", l),
Constraint::Fill(l) => write!(f, "Fill({})", l),
Constraint::Max(m) => write!(f, "Max({})", m),
Constraint::Min(m) => write!(f, "Min({})", m),
Self::Percentage(p) => write!(f, "Percentage({p})"),
Self::Ratio(n, d) => write!(f, "Ratio({n}, {d})"),
Self::Length(l) => write!(f, "Length({l})"),
Self::Fill(l) => write!(f, "Fill({l})"),
Self::Max(m) => write!(f, "Max({m})"),
Self::Min(m) => write!(f, "Min({m})"),
}
}
}

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

@@ -1,21 +1,22 @@
use std::fmt::{self, Display};
use std::fmt;
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Margin {
pub horizontal: u16,
pub vertical: u16,
}
impl Margin {
pub const fn new(horizontal: u16, vertical: u16) -> Margin {
Margin {
pub const fn new(horizontal: u16, vertical: u16) -> Self {
Self {
horizontal,
vertical,
}
}
}
impl Display for Margin {
impl fmt::Display for Margin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}x{}", self.horizontal, self.vertical)
}

View File

@@ -1,4 +1,6 @@
#![warn(missing_docs)]
use std::fmt;
use crate::layout::Rect;
/// Position in the terminal
@@ -10,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 };
@@ -22,6 +24,7 @@ use crate::layout::Rect;
/// let (x, y) = position.into();
/// ```
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Position {
/// The x coordinate of the position
///
@@ -37,15 +40,18 @@ pub struct Position {
}
impl Position {
/// Position at the origin, the top left edge at 0,0
pub const ORIGIN: Self = Self { x: 0, y: 0 };
/// Create a new position
pub fn new(x: u16, y: u16) -> Self {
Position { x, y }
pub const fn new(x: u16, y: u16) -> Self {
Self { x, y }
}
}
impl From<(u16, u16)> for Position {
fn from((x, y): (u16, u16)) -> Self {
Position { x, y }
Self { x, y }
}
}
@@ -61,6 +67,12 @@ impl From<Rect> for Position {
}
}
impl fmt::Display for Position {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -94,4 +106,10 @@ mod tests {
assert_eq!(position.x, 1);
assert_eq!(position.y, 2);
}
#[test]
fn to_string() {
let position = Position::new(1, 2);
assert_eq!(position.to_string(), "(1, 2)");
}
}

View File

@@ -4,78 +4,40 @@ use std::{
fmt,
};
use layout::{Position, Size};
use crate::layout::{Margin, Position, Size};
use crate::prelude::*;
mod offset;
pub use offset::*;
mod iter;
pub use iter::*;
/// A Rectangular area.
///
/// A simple rectangle used in the computation of the layout and to give widgets a hint about the
/// area they are supposed to render to.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Rect {
/// The x coordinate of the top left corner of the rect.
/// The x coordinate of the top left corner of the `Rect`.
pub x: u16,
/// The y coordinate of the top left corner of the rect.
/// The y coordinate of the top left corner of the `Rect`.
pub y: u16,
/// The width of the rect.
/// The width of the `Rect`.
pub width: u16,
/// The height of the rect.
/// The height of the `Rect`.
pub height: u16,
}
/// Manages row divisions within a `Rect`.
/// Amounts by which to move a [`Rect`](crate::layout::Rect).
///
/// The `Rows` struct is an iterator that allows iterating through rows of a given `Rect`.
pub struct Rows {
/// The `Rect` associated with the rows.
pub rect: Rect,
/// The y coordinate of the row within the `Rect`.
pub current_row: u16,
}
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 >= self.rect.bottom() {
return None;
}
let row = Rect::new(self.rect.x, self.current_row, self.rect.width, 1);
self.current_row += 1;
Some(row)
}
}
/// Manages column divisions within a `Rect`.
/// Positive numbers move to the right/bottom and negative to the left/top.
///
/// The `Columns` struct is an iterator that allows iterating through columns of a given `Rect`.
pub struct Columns {
/// The `Rect` associated with the columns.
pub rect: Rect,
/// The x coordinate of the column within the `Rect`.
pub current_column: u16,
}
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 >= self.rect.right() {
return None;
}
let column = Rect::new(self.current_column, self.rect.y, 1, self.rect.height);
self.current_column += 1;
Some(column)
}
/// See [`Rect::offset`]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Offset {
/// How much to move on the X axis
pub x: i32,
/// How much to move on the Y axis
pub y: i32,
}
impl fmt::Display for Rect {
@@ -85,76 +47,96 @@ impl fmt::Display for Rect {
}
impl Rect {
/// Creates a new rect, with width and height limited to keep the area under max u16. If
/// clipped, aspect ratio will be preserved.
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
let max_area = u16::max_value();
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)
};
Rect {
/// A zero sized Rect at position 0,0
pub const ZERO: Self = Self {
x: 0,
y: 0,
width: 0,
height: 0,
};
/// 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)
/// 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) -> u32 {
(self.width as u32) * (self.height as u32)
}
/// Returns true if the rect has no area.
/// Returns true if the `Rect` has no area.
pub const fn is_empty(self) -> bool {
self.width == 0 || self.height == 0
}
/// Returns the left coordinate of the rect.
/// Returns the left coordinate of the `Rect`.
pub const fn left(self) -> u16 {
self.x
}
/// Returns the right coordinate of the rect. This is the first coordinate outside of the rect.
/// Returns the right coordinate of the `Rect`. This is the first coordinate outside of the
/// `Rect`.
///
/// If the right coordinate is larger than the maximum value of u16, it will be clamped to
/// u16::MAX.
/// `u16::MAX`.
pub const fn right(self) -> u16 {
self.x.saturating_add(self.width)
}
/// Returns the top coordinate of the rect.
/// Returns the top coordinate of the `Rect`.
pub const fn top(self) -> u16 {
self.y
}
/// Returns the bottom coordinate of the rect. This is the first coordinate outside of the rect.
/// Returns the bottom coordinate of the `Rect`. This is the first coordinate outside of the
/// `Rect`.
///
/// If the bottom coordinate is larger than the maximum value of u16, it will be clamped to
/// u16::MAX.
/// `u16::MAX`.
pub const fn bottom(self) -> u16 {
self.y.saturating_add(self.height)
}
/// Returns a new rect inside the current one, with the given margin on each side.
/// Returns a new `Rect` inside the current one, with the given margin on each side.
///
/// If the margin is larger than the rect, the returned rect will have no area.
pub fn inner(self, margin: &Margin) -> Rect {
/// If the margin is larger than the `Rect`, the returned `Rect` will have no area.
#[must_use = "method returns the modified value"]
pub const fn inner(self, margin: Margin) -> Self {
let doubled_margin_horizontal = margin.horizontal.saturating_mul(2);
let doubled_margin_vertical = margin.vertical.saturating_mul(2);
if self.width < doubled_margin_horizontal || self.height < doubled_margin_vertical {
Rect::default()
Self::ZERO
} else {
Rect {
Self {
x: self.x.saturating_add(margin.horizontal),
y: self.y.saturating_add(margin.vertical),
width: self.width.saturating_sub(doubled_margin_horizontal),
@@ -171,25 +153,27 @@ impl Rect {
/// - Positive `y` moves the whole `Rect` to the bottom, negative to the top.
///
/// See [`Offset`] for details.
pub fn offset(self, offset: Offset) -> Rect {
Rect {
#[must_use = "method returns the modified value"]
pub fn offset(self, offset: Offset) -> Self {
Self {
x: i32::from(self.x)
.saturating_add(offset.x)
.clamp(0, (u16::MAX - self.width) as i32) as u16,
.clamp(0, i32::from(u16::MAX - self.width)) as u16,
y: i32::from(self.y)
.saturating_add(offset.y)
.clamp(0, (u16::MAX - self.height) as i32) as u16,
.clamp(0, i32::from(u16::MAX - self.height)) as u16,
..self
}
}
/// Returns a new rect that contains both the current one and the given one.
pub fn union(self, other: Rect) -> Rect {
/// Returns a new `Rect` that contains both the current one and the given one.
#[must_use = "method returns the modified value"]
pub fn union(self, other: Self) -> Self {
let x1 = min(self.x, other.x);
let y1 = min(self.y, other.y);
let x2 = max(self.right(), other.right());
let y2 = max(self.bottom(), other.bottom());
Rect {
Self {
x: x1,
y: y1,
width: x2.saturating_sub(x1),
@@ -197,15 +181,16 @@ impl Rect {
}
}
/// Returns a new rect that is the intersection of the current one and the given one.
/// Returns a new `Rect` that is the intersection of the current one and the given one.
///
/// If the two rects do not intersect, the returned rect will have no area.
pub fn intersection(self, other: Rect) -> Rect {
/// If the two `Rect`s do not intersect, the returned `Rect` will have no area.
#[must_use = "method returns the modified value"]
pub fn intersection(self, other: Self) -> Self {
let x1 = max(self.x, other.x);
let y1 = max(self.y, other.y);
let x2 = min(self.right(), other.right());
let y2 = min(self.bottom(), other.bottom());
Rect {
Self {
x: x1,
y: y1,
width: x2.saturating_sub(x1),
@@ -213,22 +198,23 @@ impl Rect {
}
}
/// Returns true if the two rects intersect.
pub const fn intersects(self, other: Rect) -> bool {
/// Returns true if the two `Rect`s intersect.
pub const fn intersects(self, other: Self) -> bool {
self.x < other.right()
&& self.right() > other.x
&& self.y < other.bottom()
&& self.bottom() > other.y
}
/// Returns true if the given position is inside the rect.
/// Returns true if the given position is inside the `Rect`.
///
/// The position is considered inside the rect if it is on the rect's border.
/// The position is considered inside the `Rect` if it is on the `Rect`'s border.
///
/// # 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 }));
/// ````
@@ -239,109 +225,133 @@ impl Rect {
&& position.y < self.bottom()
}
/// Clamp this rect to fit inside the other rect.
/// Clamp this `Rect` to fit inside the other `Rect`.
///
/// If the width or height of this rect is larger than the other rect, it will be clamped to the
/// other rect's width or height.
/// If the width or height of this `Rect` is larger than the other `Rect`, it will be clamped to
/// the other `Rect`'s width or height.
///
/// If the left or top coordinate of this rect is smaller than the other rect, it will be
/// clamped to the other rect's left or top coordinate.
/// If the left or top coordinate of this `Rect` is smaller than the other `Rect`, it will be
/// clamped to the other `Rect`'s left or top coordinate.
///
/// If the right or bottom coordinate of this rect is larger than the other rect, it will be
/// clamped to the other rect's right or bottom coordinate.
/// If the right or bottom coordinate of this `Rect` is larger than the other `Rect`, it will be
/// clamped to the other `Rect`'s right or bottom coordinate.
///
/// This is different from [`Rect::intersection`] because it will move this rect to fit inside
/// the other rect, while [`Rect::intersection`] instead would keep this rect's position and
/// truncate its size to only that which is inside the other rect.
/// This is different from [`Rect::intersection`] because it will move this `Rect` to fit inside
/// the other `Rect`, while [`Rect::intersection`] instead would keep this `Rect`'s position and
/// truncate its size to only that which is inside the other `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));
/// ```
pub fn clamp(self, other: Rect) -> Rect {
#[must_use = "method returns the modified value"]
pub fn clamp(self, other: Self) -> Self {
let width = self.width.min(other.width);
let height = self.height.min(other.height);
let x = self.x.clamp(other.x, other.right().saturating_sub(width));
let y = self.y.clamp(other.y, other.bottom().saturating_sub(height));
Rect::new(x, y, width, height)
Self::new(x, y, width, height)
}
/// Creates an iterator over rows within the `Rect`.
/// An iterator over rows within the `Rect`.
///
/// This method returns a `Rows` iterator that allows iterating through rows of the `Rect`.
///
/// # Examples
/// # Example
///
/// ```
/// use ratatui::prelude::*;
/// let area = Rect::new(0, 0, 10, 5);
/// for row in area.rows() {
/// // Perform operations on each row of the area
/// println!("Row: {:?}", row);
/// 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);
/// }
/// }
/// ```
pub fn rows(&self) -> Rows {
Rows {
rect: *self,
current_row: self.y,
}
pub const fn rows(self) -> Rows {
Rows::new(self)
}
/// Creates an iterator over columns within the `Rect`.
/// An iterator over columns within the `Rect`.
///
/// This method returns a `Columns` iterator that allows iterating through columns of the
/// `Rect`.
///
/// # Examples
/// # Example
///
/// ```
/// use ratatui::prelude::*;
/// let area = Rect::new(0, 0, 10, 5);
/// for column in area.columns() {
/// // Perform operations on each column of the area
/// println!("Column: {:?}", column);
/// use ratatui_core::{buffer::Buffer, layout::Rect, text::Text, widgets::Widget};
///
/// fn render(area: Rect, buf: &mut Buffer) {
/// for (i, column) in area.columns().enumerate() {
/// Text::from(format!("{}", i)).render(column, buf);
/// }
/// }
/// ```
pub fn columns(&self) -> Columns {
Columns {
rect: *self,
current_column: self.x,
}
pub const fn columns(self) -> Columns {
Columns::new(self)
}
/// Returns a [`Position`] with the same coordinates as this rect.
/// An iterator over the positions within the `Rect`.
///
/// The positions are returned in a row-major order (left-to-right, top-to-bottom).
///
/// # Example
///
/// ```
/// 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");
/// }
/// }
/// ```
pub const fn positions(self) -> Positions {
Positions::new(self)
}
/// Returns a [`Position`] with the same coordinates as this `Rect`.
///
/// # Examples
///
/// ```
/// # use ratatui::prelude::*;
/// use ratatui_core::layout::Rect;
///
/// let rect = Rect::new(1, 2, 3, 4);
/// let position = rect.as_position();
/// ````
pub fn as_position(self) -> Position {
pub const fn as_position(self) -> Position {
Position {
x: self.x,
y: self.y,
}
}
/// Converts the rect into a size struct.
pub fn as_size(self) -> Size {
/// Converts the `Rect` into a size struct.
pub const fn as_size(self) -> Size {
Size {
width: self.width,
height: self.height,
}
}
/// indents the x value of the `Rect` by a given `offset`
///
/// This is pub(crate) for now as we need to stabilize the naming / design of this API.
#[must_use]
pub(crate) const fn indent_x(self, offset: u16) -> Self {
Self {
x: self.x.saturating_add(offset),
width: self.width.saturating_sub(offset),
..self
}
}
}
impl From<(Position, Size)> for Rect {
fn from((position, size): (Position, Size)) -> Self {
Rect {
Self {
x: position.x,
y: position.y,
width: size.width,
@@ -355,6 +365,7 @@ mod tests {
use rstest::rstest;
use super::*;
use crate::layout::{Constraint, Layout};
#[test]
fn to_string() {
@@ -409,7 +420,7 @@ mod tests {
#[test]
fn inner() {
assert_eq!(
Rect::new(1, 2, 3, 4).inner(&Margin::new(1, 2)),
Rect::new(1, 2, 3, 4).inner(Margin::new(1, 2)),
Rect::new(2, 4, 1, 0)
);
}
@@ -499,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]
@@ -549,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,11 +1,14 @@
#![warn(missing_docs)]
use crate::prelude::*;
use std::fmt;
use crate::layout::Rect;
/// A simple size struct
///
/// The width and height are stored as `u16` values and represent the number of columns and rows
/// respectively.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Size {
/// The width in columns
pub width: u16,
@@ -14,15 +17,18 @@ pub struct Size {
}
impl Size {
/// A zero sized Size
pub const ZERO: Self = Self::new(0, 0);
/// Create a new `Size` struct
pub fn new(width: u16, height: u16) -> Self {
Size { width, height }
pub const fn new(width: u16, height: u16) -> Self {
Self { width, height }
}
}
impl From<(u16, u16)> for Size {
fn from((width, height): (u16, u16)) -> Self {
Size { width, height }
Self { width, height }
}
}
@@ -32,6 +38,12 @@ impl From<Rect> for Size {
}
}
impl fmt::Display for Size {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}x{}", self.width, self.height)
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -56,4 +68,9 @@ mod tests {
assert_eq!(size.width, 10);
assert_eq!(size.height, 20);
}
#[test]
fn display() {
assert_eq!(Size::new(10, 20).to_string(), "10x20");
}
}

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)
@@ -33,15 +36,17 @@
//! that implements [`Styled`]. E.g.:
//! - Strings and string slices when styled return a [`Span`]
//! - [`Span`]s can be styled again, which will merge the styles.
//! - Many widget types can be styled directly rather than calling their style() method.
//! - 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,19 +70,20 @@
//! );
//! ```
//!
//! [`prelude`]: crate::prelude
//! [`Span`]: crate::text::Span
use std::fmt::{self, Debug};
use std::fmt;
use bitflags::bitflags;
pub use color::{Color, ParseColorError};
use stylize::ColorDebugKind;
pub use stylize::{Styled, Stylize};
mod color;
mod stylize;
pub use color::Color;
pub use stylize::{Styled, Stylize};
pub mod palette;
#[cfg(feature = "palette")]
mod palette_conversion;
mod stylize;
bitflags! {
/// Modifier changes the way a piece of text is displayed.
@@ -90,7 +96,7 @@ bitflags! {
/// ## Examples
///
/// ```rust
/// use ratatui::{prelude::*};
/// use ratatui_core::style::Modifier;
///
/// let m = Modifier::BOLD | Modifier::ITALIC;
/// ```
@@ -119,14 +125,14 @@ impl fmt::Debug for Modifier {
if self.is_empty() {
return write!(f, "NONE");
}
fmt::Debug::fmt(&self.0, f)
write!(f, "{}", self.0)
}
}
/// 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)
@@ -137,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();
/// ```
///
@@ -147,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);
@@ -162,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()
@@ -179,7 +194,7 @@ impl fmt::Debug for Modifier {
/// ];
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
/// for style in &styles {
/// buffer.get_mut(0, 0).set_style(*style);
/// buffer[(0, 0)].set_style(*style);
/// }
/// assert_eq!(
/// Style {
@@ -190,7 +205,7 @@ impl fmt::Debug for Modifier {
/// add_modifier: Modifier::BOLD | Modifier::UNDERLINED,
/// sub_modifier: Modifier::empty(),
/// },
/// buffer.get(0, 0).style(),
/// buffer[(0, 0)].style(),
/// );
/// ```
///
@@ -198,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()
@@ -208,7 +227,7 @@ impl fmt::Debug for Modifier {
/// ];
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
/// for style in &styles {
/// buffer.get_mut(0, 0).set_style(*style);
/// buffer[(0, 0)].set_style(*style);
/// }
/// assert_eq!(
/// Style {
@@ -219,41 +238,51 @@ impl fmt::Debug for Modifier {
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
/// buffer.get(0, 0).style(),
/// buffer[(0, 0)].style(),
/// );
/// ```
#[derive(Debug, 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,
}
impl Default for Style {
fn default() -> Style {
Style::new()
/// 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 = Style;
type Item = Self;
fn style(&self) -> Style {
*self
}
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
fn set_style<S: Into<Self>>(self, style: S) -> Self::Item {
self.patch(style)
}
}
impl Style {
pub const fn new() -> Style {
Style {
/// Returns a `Style` with default properties.
pub const fn new() -> Self {
Self {
fg: None,
bg: None,
#[cfg(feature = "underline-color")]
@@ -264,8 +293,8 @@ impl Style {
}
/// Returns a `Style` resetting all properties.
pub const fn reset() -> Style {
Style {
pub const fn reset() -> Self {
Self {
fg: Some(Color::Reset),
bg: Some(Color::Reset),
#[cfg(feature = "underline-color")]
@@ -280,13 +309,14 @@ 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));
/// ```
#[must_use = "`fg` returns the modified style without modifying the original"]
pub const fn fg(mut self, color: Color) -> Style {
pub const fn fg(mut self, color: Color) -> Self {
self.fg = Some(color);
self
}
@@ -296,13 +326,14 @@ 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));
/// ```
#[must_use = "`bg` returns the modified style without modifying the original"]
pub const fn bg(mut self, color: Color) -> Style {
pub const fn bg(mut self, color: Color) -> Self {
self.bg = Some(color);
self
}
@@ -320,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);
@@ -336,7 +368,7 @@ impl Style {
/// ```
#[cfg(feature = "underline-color")]
#[must_use = "`underline_color` returns the modified style without modifying the original"]
pub const fn underline_color(mut self, color: Color) -> Style {
pub const fn underline_color(mut self, color: Color) -> Self {
self.underline_color = Some(color);
self
}
@@ -348,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);
@@ -356,7 +389,7 @@ impl Style {
/// assert_eq!(patched.sub_modifier, Modifier::empty());
/// ```
#[must_use = "`add_modifier` returns the modified style without modifying the original"]
pub const fn add_modifier(mut self, modifier: Modifier) -> Style {
pub const fn add_modifier(mut self, modifier: Modifier) -> Self {
self.sub_modifier = self.sub_modifier.difference(modifier);
self.add_modifier = self.add_modifier.union(modifier);
self
@@ -369,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);
@@ -377,7 +411,7 @@ impl Style {
/// assert_eq!(patched.sub_modifier, Modifier::ITALIC);
/// ```
#[must_use = "`remove_modifier` returns the modified style without modifying the original"]
pub const fn remove_modifier(mut self, modifier: Modifier) -> Style {
pub const fn remove_modifier(mut self, modifier: Modifier) -> Self {
self.add_modifier = self.add_modifier.difference(modifier);
self.sub_modifier = self.sub_modifier.union(modifier);
self
@@ -391,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);
@@ -401,7 +436,7 @@ impl Style {
/// );
/// ```
#[must_use = "`patch` returns the modified style without modifying the original"]
pub fn patch<S: Into<Style>>(mut self, other: S) -> Style {
pub fn patch<S: Into<Self>>(mut self, other: S) -> Self {
let other = other.into();
self.fg = other.fg.or(self.fg);
self.bg = other.bg.or(self.bg);
@@ -418,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 {
@@ -428,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 {
@@ -442,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
@@ -464,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 {
@@ -478,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));
/// ```
@@ -497,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));
/// ```
@@ -514,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));
/// ```
@@ -530,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,
@@ -550,34 +640,44 @@ impl From<(Color, Color, Modifier, Modifier)> for Style {
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
fn styles() -> Vec<Style> {
vec![
Style::default(),
Style::default().fg(Color::Yellow),
Style::default().bg(Color::Yellow),
Style::default().add_modifier(Modifier::BOLD),
Style::default().remove_modifier(Modifier::BOLD),
Style::default().add_modifier(Modifier::ITALIC),
Style::default().remove_modifier(Modifier::ITALIC),
Style::default().add_modifier(Modifier::ITALIC | Modifier::BOLD),
Style::default().remove_modifier(Modifier::ITALIC | Modifier::BOLD),
]
#[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 = styles();
let styles = [
Style::new(),
Style::new().fg(Color::Yellow),
Style::new().bg(Color::Yellow),
Style::new().add_modifier(Modifier::BOLD),
Style::new().remove_modifier(Modifier::BOLD),
Style::new().add_modifier(Modifier::ITALIC),
Style::new().remove_modifier(Modifier::ITALIC),
Style::new().add_modifier(Modifier::ITALIC | Modifier::BOLD),
Style::new().remove_modifier(Modifier::ITALIC | Modifier::BOLD),
];
for &a in &styles {
for &b in &styles {
for &c in &styles {
for &d in &styles {
let combined = a.patch(b.patch(c.patch(d)));
assert_eq!(
Style::default().patch(a).patch(b).patch(c).patch(d),
Style::default().patch(combined)
Style::new().patch(a).patch(b).patch(c).patch(d),
Style::new().patch(a.patch(b.patch(c.patch(d))))
);
}
}
@@ -589,7 +689,7 @@ mod tests {
fn combine_individual_modifiers() {
use crate::{buffer::Buffer, layout::Rect};
let mods = vec![
let mods = [
Modifier::BOLD,
Modifier::DIM,
Modifier::ITALIC,
@@ -603,37 +703,30 @@ mod tests {
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
for m in &mods {
buffer.get_mut(0, 0).set_style(Style::reset());
buffer
.get_mut(0, 0)
.set_style(Style::default().add_modifier(*m));
let style = buffer.get(0, 0).style();
assert!(style.add_modifier.contains(*m));
assert!(!style.sub_modifier.contains(*m));
for m in mods {
buffer[(0, 0)].set_style(Style::reset());
buffer[(0, 0)].set_style(Style::new().add_modifier(m));
let style = buffer[(0, 0)].style();
assert!(style.add_modifier.contains(m));
assert!(!style.sub_modifier.contains(m));
}
}
#[test]
fn modifier_debug() {
assert_eq!(format!("{:?}", Modifier::empty()), "NONE");
assert_eq!(format!("{:?}", Modifier::BOLD), "BOLD");
assert_eq!(format!("{:?}", Modifier::DIM), "DIM");
assert_eq!(format!("{:?}", Modifier::ITALIC), "ITALIC");
assert_eq!(format!("{:?}", Modifier::UNDERLINED), "UNDERLINED");
assert_eq!(format!("{:?}", Modifier::SLOW_BLINK), "SLOW_BLINK");
assert_eq!(format!("{:?}", Modifier::RAPID_BLINK), "RAPID_BLINK");
assert_eq!(format!("{:?}", Modifier::REVERSED), "REVERSED");
assert_eq!(format!("{:?}", Modifier::HIDDEN), "HIDDEN");
assert_eq!(format!("{:?}", Modifier::CROSSED_OUT), "CROSSED_OUT");
assert_eq!(
format!("{:?}", Modifier::BOLD | Modifier::DIM),
"BOLD | DIM"
);
assert_eq!(
format!("{:?}", Modifier::all()),
"BOLD | DIM | ITALIC | UNDERLINED | SLOW_BLINK | RAPID_BLINK | REVERSED | HIDDEN | CROSSED_OUT"
);
#[rstest]
#[case(Modifier::empty(), "NONE")]
#[case(Modifier::BOLD, "BOLD")]
#[case(Modifier::DIM, "DIM")]
#[case(Modifier::ITALIC, "ITALIC")]
#[case(Modifier::UNDERLINED, "UNDERLINED")]
#[case(Modifier::SLOW_BLINK, "SLOW_BLINK")]
#[case(Modifier::RAPID_BLINK, "RAPID_BLINK")]
#[case(Modifier::REVERSED, "REVERSED")]
#[case(Modifier::HIDDEN, "HIDDEN")]
#[case(Modifier::CROSSED_OUT, "CROSSED_OUT")]
#[case(Modifier::BOLD | Modifier::DIM, "BOLD | DIM")]
#[case(Modifier::all(), "BOLD | DIM | ITALIC | UNDERLINED | SLOW_BLINK | RAPID_BLINK | REVERSED | HIDDEN | CROSSED_OUT")]
fn modifier_debug(#[case] modifier: Modifier, #[case] expected: &str) {
assert_eq!(format!("{modifier:?}"), expected);
}
#[test]
@@ -660,153 +753,83 @@ mod tests {
.bg(Color::Black)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::ITALIC)
)
);
}
#[rstest]
#[case(Style::new().black(), Color::Black)]
#[case(Style::new().red(), Color::Red)]
#[case(Style::new().green(), Color::Green)]
#[case(Style::new().yellow(), Color::Yellow)]
#[case(Style::new().blue(), Color::Blue)]
#[case(Style::new().magenta(), Color::Magenta)]
#[case(Style::new().cyan(), Color::Cyan)]
#[case(Style::new().white(), Color::White)]
#[case(Style::new().gray(), Color::Gray)]
#[case(Style::new().dark_gray(), Color::DarkGray)]
#[case(Style::new().light_red(), Color::LightRed)]
#[case(Style::new().light_green(), Color::LightGreen)]
#[case(Style::new().light_yellow(), Color::LightYellow)]
#[case(Style::new().light_blue(), Color::LightBlue)]
#[case(Style::new().light_magenta(), Color::LightMagenta)]
#[case(Style::new().light_cyan(), Color::LightCyan)]
#[case(Style::new().white(), Color::White)]
fn fg_can_be_stylized(#[case] stylized: Style, #[case] expected: Color) {
assert_eq!(stylized, Style::new().fg(expected));
}
#[rstest]
#[case(Style::new().on_black(), Color::Black)]
#[case(Style::new().on_red(), Color::Red)]
#[case(Style::new().on_green(), Color::Green)]
#[case(Style::new().on_yellow(), Color::Yellow)]
#[case(Style::new().on_blue(), Color::Blue)]
#[case(Style::new().on_magenta(), Color::Magenta)]
#[case(Style::new().on_cyan(), Color::Cyan)]
#[case(Style::new().on_white(), Color::White)]
#[case(Style::new().on_gray(), Color::Gray)]
#[case(Style::new().on_dark_gray(), Color::DarkGray)]
#[case(Style::new().on_light_red(), Color::LightRed)]
#[case(Style::new().on_light_green(), Color::LightGreen)]
#[case(Style::new().on_light_yellow(), Color::LightYellow)]
#[case(Style::new().on_light_blue(), Color::LightBlue)]
#[case(Style::new().on_light_magenta(), Color::LightMagenta)]
#[case(Style::new().on_light_cyan(), Color::LightCyan)]
#[case(Style::new().on_white(), Color::White)]
fn bg_can_be_stylized(#[case] stylized: Style, #[case] expected: Color) {
assert_eq!(stylized, Style::new().bg(expected));
}
#[rstest]
#[case(Style::new().bold(), Modifier::BOLD)]
#[case(Style::new().dim(), Modifier::DIM)]
#[case(Style::new().italic(), Modifier::ITALIC)]
#[case(Style::new().underlined(), Modifier::UNDERLINED)]
#[case(Style::new().slow_blink(), Modifier::SLOW_BLINK)]
#[case(Style::new().rapid_blink(), Modifier::RAPID_BLINK)]
#[case(Style::new().reversed(), Modifier::REVERSED)]
#[case(Style::new().hidden(), Modifier::HIDDEN)]
#[case(Style::new().crossed_out(), Modifier::CROSSED_OUT)]
fn add_modifier_can_be_stylized(#[case] stylized: Style, #[case] expected: Modifier) {
assert_eq!(stylized, Style::new().add_modifier(expected));
}
#[rstest]
#[case(Style::new().not_bold(), Modifier::BOLD)]
#[case(Style::new().not_dim(), Modifier::DIM)]
#[case(Style::new().not_italic(), Modifier::ITALIC)]
#[case(Style::new().not_underlined(), Modifier::UNDERLINED)]
#[case(Style::new().not_slow_blink(), Modifier::SLOW_BLINK)]
#[case(Style::new().not_rapid_blink(), Modifier::RAPID_BLINK)]
#[case(Style::new().not_reversed(), Modifier::REVERSED)]
#[case(Style::new().not_hidden(), Modifier::HIDDEN)]
#[case(Style::new().not_crossed_out(), Modifier::CROSSED_OUT)]
fn remove_modifier_can_be_stylized(#[case] stylized: Style, #[case] expected: Modifier) {
assert_eq!(stylized, Style::new().remove_modifier(expected));
}
#[test]
fn style_can_be_stylized() {
// foreground colors
assert_eq!(Style::new().black(), Style::new().fg(Color::Black));
assert_eq!(Style::new().red(), Style::new().fg(Color::Red));
assert_eq!(Style::new().green(), Style::new().fg(Color::Green));
assert_eq!(Style::new().yellow(), Style::new().fg(Color::Yellow));
assert_eq!(Style::new().blue(), Style::new().fg(Color::Blue));
assert_eq!(Style::new().magenta(), Style::new().fg(Color::Magenta));
assert_eq!(Style::new().cyan(), Style::new().fg(Color::Cyan));
assert_eq!(Style::new().white(), Style::new().fg(Color::White));
assert_eq!(Style::new().gray(), Style::new().fg(Color::Gray));
assert_eq!(Style::new().dark_gray(), Style::new().fg(Color::DarkGray));
assert_eq!(Style::new().light_red(), Style::new().fg(Color::LightRed));
assert_eq!(
Style::new().light_green(),
Style::new().fg(Color::LightGreen)
);
assert_eq!(
Style::new().light_yellow(),
Style::new().fg(Color::LightYellow)
);
assert_eq!(Style::new().light_blue(), Style::new().fg(Color::LightBlue));
assert_eq!(
Style::new().light_magenta(),
Style::new().fg(Color::LightMagenta)
);
assert_eq!(Style::new().light_cyan(), Style::new().fg(Color::LightCyan));
assert_eq!(Style::new().white(), Style::new().fg(Color::White));
// Background colors
assert_eq!(Style::new().on_black(), Style::new().bg(Color::Black));
assert_eq!(Style::new().on_red(), Style::new().bg(Color::Red));
assert_eq!(Style::new().on_green(), Style::new().bg(Color::Green));
assert_eq!(Style::new().on_yellow(), Style::new().bg(Color::Yellow));
assert_eq!(Style::new().on_blue(), Style::new().bg(Color::Blue));
assert_eq!(Style::new().on_magenta(), Style::new().bg(Color::Magenta));
assert_eq!(Style::new().on_cyan(), Style::new().bg(Color::Cyan));
assert_eq!(Style::new().on_white(), Style::new().bg(Color::White));
assert_eq!(Style::new().on_gray(), Style::new().bg(Color::Gray));
assert_eq!(
Style::new().on_dark_gray(),
Style::new().bg(Color::DarkGray)
);
assert_eq!(
Style::new().on_light_red(),
Style::new().bg(Color::LightRed)
);
assert_eq!(
Style::new().on_light_green(),
Style::new().bg(Color::LightGreen)
);
assert_eq!(
Style::new().on_light_yellow(),
Style::new().bg(Color::LightYellow)
);
assert_eq!(
Style::new().on_light_blue(),
Style::new().bg(Color::LightBlue)
);
assert_eq!(
Style::new().on_light_magenta(),
Style::new().bg(Color::LightMagenta)
);
assert_eq!(
Style::new().on_light_cyan(),
Style::new().bg(Color::LightCyan)
);
assert_eq!(Style::new().on_white(), Style::new().bg(Color::White));
// Add Modifiers
assert_eq!(
Style::new().bold(),
Style::new().add_modifier(Modifier::BOLD)
);
assert_eq!(Style::new().dim(), Style::new().add_modifier(Modifier::DIM));
assert_eq!(
Style::new().italic(),
Style::new().add_modifier(Modifier::ITALIC)
);
assert_eq!(
Style::new().underlined(),
Style::new().add_modifier(Modifier::UNDERLINED)
);
assert_eq!(
Style::new().slow_blink(),
Style::new().add_modifier(Modifier::SLOW_BLINK)
);
assert_eq!(
Style::new().rapid_blink(),
Style::new().add_modifier(Modifier::RAPID_BLINK)
);
assert_eq!(
Style::new().reversed(),
Style::new().add_modifier(Modifier::REVERSED)
);
assert_eq!(
Style::new().hidden(),
Style::new().add_modifier(Modifier::HIDDEN)
);
assert_eq!(
Style::new().crossed_out(),
Style::new().add_modifier(Modifier::CROSSED_OUT)
);
// Remove Modifiers
assert_eq!(
Style::new().not_bold(),
Style::new().remove_modifier(Modifier::BOLD)
);
assert_eq!(
Style::new().not_dim(),
Style::new().remove_modifier(Modifier::DIM)
);
assert_eq!(
Style::new().not_italic(),
Style::new().remove_modifier(Modifier::ITALIC)
);
assert_eq!(
Style::new().not_underlined(),
Style::new().remove_modifier(Modifier::UNDERLINED)
);
assert_eq!(
Style::new().not_slow_blink(),
Style::new().remove_modifier(Modifier::SLOW_BLINK)
);
assert_eq!(
Style::new().not_rapid_blink(),
Style::new().remove_modifier(Modifier::RAPID_BLINK)
);
assert_eq!(
Style::new().not_reversed(),
Style::new().remove_modifier(Modifier::REVERSED)
);
assert_eq!(
Style::new().not_hidden(),
Style::new().remove_modifier(Modifier::HIDDEN)
);
assert_eq!(
Style::new().not_crossed_out(),
Style::new().remove_modifier(Modifier::CROSSED_OUT)
);
// reset
fn reset_can_be_stylized() {
assert_eq!(Style::new().reset(), Style::reset());
}

View File

@@ -1,7 +1,8 @@
use std::{
fmt::{self, Debug, Display},
str::FromStr,
};
#![allow(clippy::unreadable_literal)]
use std::{fmt, str::FromStr};
use crate::style::stylize::{ColorDebug, ColorDebugKind};
/// ANSI Color
///
@@ -43,7 +44,7 @@ use std::{
/// ```
/// 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));
@@ -64,7 +65,6 @@ use std::{
///
/// [ANSI color table]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum Color {
/// Resets the foreground or background color
#[default]
@@ -112,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.
///
@@ -131,22 +129,112 @@ impl Color {
/// Convert a u32 to a Color
///
/// The u32 should be in the format 0x00RRGGBB.
pub const fn from_u32(u: u32) -> Color {
pub const fn from_u32(u: u32) -> Self {
let r = (u >> 16) as u8;
let g = (u >> 8) as u8;
let b = u as u8;
Color::Rgb(r, g, b)
Self::Rgb(r, g, b)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for Color {
/// This utilises the [`fmt::Display`] implementation for serialization.
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Color {
/// This is used to deserialize a value into Color via serde.
///
/// This implementation uses the `FromStr` trait to deserialize strings, so named colours, RGB,
/// and indexed values are able to be deserialized. In addition, values that were produced by
/// the the older serialization implementation of Color are also able to be deserialized.
///
/// Prior to v0.26.0, Ratatui would be serialized using a map for indexed and RGB values, for
/// examples in json `{"Indexed": 10}` and `{"Rgb": [255, 0, 255]}` respectively. Now they are
/// serialized using the string representation of the index and the RGB hex value, for example
/// in json it would now be `"10"` and `"#FF00FF"` respectively.
///
/// See the [`Color`] documentation for more information on color names.
///
/// # Examples
///
/// ```
/// use std::str::FromStr;
///
/// use ratatui_core::style::Color;
///
/// #[derive(Debug, serde::Deserialize)]
/// struct Theme {
/// color: Color,
/// }
///
/// # fn get_theme() -> Result<(), serde_json::Error> {
/// let theme: Theme = serde_json::from_str(r#"{"color": "bright-white"}"#)?;
/// assert_eq!(theme.color, Color::White);
///
/// let theme: Theme = serde_json::from_str(r##"{"color": "#00FF00"}"##)?;
/// assert_eq!(theme.color, Color::Rgb(0, 255, 0));
///
/// let theme: Theme = serde_json::from_str(r#"{"color": "42"}"#)?;
/// assert_eq!(theme.color, Color::Indexed(42));
///
/// let err = serde_json::from_str::<Theme>(r#"{"color": "invalid"}"#).unwrap_err();
/// assert!(err.is_data());
/// assert_eq!(
/// err.to_string(),
/// "Failed to parse Colors at line 1 column 20"
/// );
///
/// // Deserializing from the previous serialization implementation
/// let theme: Theme = serde_json::from_str(r#"{"color": {"Rgb":[255,0,255]}}"#)?;
/// assert_eq!(theme.color, Color::Rgb(255, 0, 255));
///
/// let theme: Theme = serde_json::from_str(r#"{"color": {"Indexed":10}}"#)?;
/// assert_eq!(theme.color, Color::Indexed(10));
/// # Ok(())
/// # }
/// ```
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
FromStr::from_str(&s).map_err(serde::de::Error::custom)
/// Colors are currently serialized with the `Display` implementation, so
/// RGB values are serialized via hex, for example "#FFFFFF".
///
/// Previously they were serialized using serde derive, which encoded
/// RGB values as a map, for example { "rgb": [255, 255, 255] }.
///
/// The deserialization implementation utilises a `Helper` struct
/// to be able to support both formats for backwards compatibility.
#[derive(serde::Deserialize)]
enum ColorWrapper {
Rgb(u8, u8, u8),
Indexed(u8),
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum ColorFormat {
V2(String),
V1(ColorWrapper),
}
let multi_type = ColorFormat::deserialize(deserializer)
.map_err(|err| serde::de::Error::custom(format!("Failed to parse Colors: {err}")))?;
match multi_type {
ColorFormat::V2(s) => FromStr::from_str(&s).map_err(serde::de::Error::custom),
ColorFormat::V1(color_wrapper) => match color_wrapper {
ColorWrapper::Rgb(red, green, blue) => Ok(Self::Rgb(red, green, blue)),
ColorWrapper::Indexed(index) => Ok(Self::Indexed(index)),
},
}
}
}
@@ -154,8 +242,8 @@ impl<'de> serde::Deserialize<'de> for Color {
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct ParseColorError;
impl std::fmt::Display for ParseColorError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl fmt::Display for ParseColorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Failed to parse Colors")
}
}
@@ -175,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);
@@ -227,16 +315,7 @@ impl FromStr for Color {
_ => {
if let Ok(index) = s.parse::<u8>() {
Self::Indexed(index)
} else if let (Ok(r), Ok(g), Ok(b)) = {
if !s.starts_with('#') || s.len() != 7 {
return Err(ParseColorError);
}
(
u8::from_str_radix(&s[1..3], 16),
u8::from_str_radix(&s[3..5], 16),
u8::from_str_radix(&s[5..7], 16),
)
} {
} else if let Some((r, g, b)) = parse_hex_color(s) {
Self::Rgb(r, g, b)
} else {
return Err(ParseColorError);
@@ -247,138 +326,151 @@ impl FromStr for Color {
}
}
impl Display for Color {
fn parse_hex_color(input: &str) -> Option<(u8, u8, u8)> {
if !input.starts_with('#') || input.len() != 7 {
return None;
}
let r = u8::from_str_radix(input.get(1..3)?, 16).ok()?;
let g = u8::from_str_radix(input.get(3..5)?, 16).ok()?;
let b = u8::from_str_radix(input.get(5..7)?, 16).ok()?;
Some((r, g, b))
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Color::Reset => write!(f, "Reset"),
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, "DarkGray"),
Color::LightRed => write!(f, "LightRed"),
Color::LightGreen => write!(f, "LightGreen"),
Color::LightYellow => write!(f, "LightYellow"),
Color::LightBlue => write!(f, "LightBlue"),
Color::LightMagenta => write!(f, "LightMagenta"),
Color::LightCyan => write!(f, "LightCyan"),
Color::White => write!(f, "White"),
Color::Rgb(r, g, b) => write!(f, "#{:02X}{:02X}{:02X}", r, g, b),
Color::Indexed(i) => write!(f, "{}", i),
Self::Reset => write!(f, "Reset"),
Self::Black => write!(f, "Black"),
Self::Red => write!(f, "Red"),
Self::Green => write!(f, "Green"),
Self::Yellow => write!(f, "Yellow"),
Self::Blue => write!(f, "Blue"),
Self::Magenta => write!(f, "Magenta"),
Self::Cyan => write!(f, "Cyan"),
Self::Gray => write!(f, "Gray"),
Self::DarkGray => write!(f, "DarkGray"),
Self::LightRed => write!(f, "LightRed"),
Self::LightGreen => write!(f, "LightGreen"),
Self::LightYellow => write!(f, "LightYellow"),
Self::LightBlue => write!(f, "LightBlue"),
Self::LightMagenta => write!(f, "LightMagenta"),
Self::LightCyan => write!(f, "LightCyan"),
Self::White => write!(f, "White"),
Self::Rgb(r, g, b) => write!(f, "#{r:02X}{g:02X}{b:02X}"),
Self::Indexed(i) => write!(f, "{i}"),
}
}
}
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(h: f64, s: f64, l: 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 r: f64;
let g: f64;
let b: f64;
// Check if the color is achromatic (grayscale)
if s == 0.0 {
r = l;
g = l;
b = l;
} else {
// Calculate RGB components for colored cases
let q = if l < 0.5 {
l * (1.0 + s)
} else {
l + s - l * s
};
let p = 2.0 * l - q;
r = hue_to_rgb(p, q, h + 1.0 / 3.0);
g = hue_to_rgb(p, q, h);
b = hue_to_rgb(p, q, h - 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(
(r * 255.0).round() as u8,
(g * 255.0).round() as u8,
(b * 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)
}
}
@@ -386,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]
@@ -501,6 +619,7 @@ mod tests {
"abcdef0", // 7 chars is not a color
" bcdefa", // doesn't start with a '#'
"#abcdef00", // too many chars
"#1🦀2", // len 7 but on char boundaries shouldnt panic
"resett", // typo
"lightblackk", // typo
];
@@ -577,4 +696,42 @@ mod tests {
Color::deserialize("#00000000".into_deserializer());
assert!(color.is_err());
}
#[cfg(feature = "serde")]
#[test]
fn serialize_then_deserialize() -> Result<(), serde_json::Error> {
let json_rgb = serde_json::to_string(&Color::Rgb(255, 0, 255))?;
assert_eq!(json_rgb, r##""#FF00FF""##);
assert_eq!(
serde_json::from_str::<Color>(&json_rgb)?,
Color::Rgb(255, 0, 255)
);
let json_white = serde_json::to_string(&Color::White)?;
assert_eq!(json_white, r#""White""#);
let json_indexed = serde_json::to_string(&Color::Indexed(10))?;
assert_eq!(json_indexed, r#""10""#);
assert_eq!(
serde_json::from_str::<Color>(&json_indexed)?,
Color::Indexed(10)
);
Ok(())
}
#[cfg(feature = "serde")]
#[test]
fn deserialize_with_previous_format() -> Result<(), serde_json::Error> {
assert_eq!(Color::White, serde_json::from_str::<Color>("\"White\"")?);
assert_eq!(
Color::Rgb(255, 0, 255),
serde_json::from_str::<Color>(r#"{"Rgb":[255,0,255]}"#)?
);
assert_eq!(
Color::Indexed(10),
serde_json::from_str::<Color>(r#"{"Indexed":10}"#)?
);
Ok(())
}
}

View File

@@ -1,3 +1,5 @@
#![allow(clippy::unreadable_literal)]
//! A module for defining color palettes.
pub mod material;

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
///
@@ -455,11 +457,11 @@ pub struct NonAccentedPalette {
}
impl AccentedPalette {
/// Create a new AccentedPalette from the given variants
/// Create a new `AccentedPalette` from the given variants
///
/// The variants should be in the format [0x00RRGGBB, ...]
pub const fn from_variants(variants: [u32; 14]) -> AccentedPalette {
AccentedPalette {
pub const fn from_variants(variants: [u32; 14]) -> Self {
Self {
c50: Color::from_u32(variants[0]),
c100: Color::from_u32(variants[1]),
c200: Color::from_u32(variants[2]),
@@ -479,11 +481,11 @@ impl AccentedPalette {
}
impl NonAccentedPalette {
/// Create a new NonAccented from the given variants
/// Create a new `NonAccented` from the given variants
///
/// The variants should be in the format [0x00RRGGBB, ...]
pub const fn from_variants(variants: [u32; 10]) -> NonAccentedPalette {
NonAccentedPalette {
pub const fn from_variants(variants: [u32; 10]) -> Self {
Self {
c50: Color::from_u32(variants[0]),
c100: Color::from_u32(variants[1]),
c200: Color::from_u32(variants[2]),

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

@@ -0,0 +1,83 @@
//! Conversions from colors in the `palette` crate to [`Color`].
use ::palette::{
bool_mask::LazySelect,
num::{Arithmetics, MulSub, PartialCmp, Powf, Real},
LinSrgb,
};
use palette::{stimulus::IntoStimulus, Srgb};
use crate::style::Color;
/// Convert an [`palette::Srgb`] color to a [`Color`].
///
/// # Examples
///
/// ```
/// use palette::Srgb;
/// 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));
/// ```
impl<T: IntoStimulus<u8>> From<Srgb<T>> for Color {
fn from(color: Srgb<T>) -> Self {
let (red, green, blue) = color.into_format().into_components();
Self::Rgb(red, green, blue)
}
}
/// Convert a [`palette::LinSrgb`] color to a [`Color`].
///
/// Note: this conversion only works for floating point linear sRGB colors. If you have a linear
/// sRGB color in another format, you need to convert it to floating point first.
///
/// # Examples
///
/// ```
/// use palette::LinSrgb;
/// 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));
/// ```
impl<T: IntoStimulus<u8>> From<LinSrgb<T>> for Color
where
T: Real + Powf + MulSub + Arithmetics + PartialCmp + Clone,
T::Mask: LazySelect<T>,
{
fn from(color: LinSrgb<T>) -> Self {
let srgb_color = Srgb::<T>::from_linear(color);
Self::from(srgb_color)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_srgb() {
const RED: Color = Color::Rgb(255, 0, 0);
assert_eq!(Color::from(Srgb::new(255u8, 0, 0)), RED);
assert_eq!(Color::from(Srgb::new(65535u16, 0, 0)), RED);
assert_eq!(Color::from(Srgb::new(1.0f32, 0.0, 0.0)), RED);
assert_eq!(
Color::from(Srgb::new(0.5f32, 0.5, 0.5)),
Color::Rgb(128, 128, 128)
);
}
#[test]
fn from_lin_srgb() {
const RED: Color = Color::Rgb(255, 0, 0);
assert_eq!(Color::from(LinSrgb::new(1.0f32, 0.0, 0.0)), RED);
assert_eq!(Color::from(LinSrgb::new(1.0f64, 0.0, 0.0)), RED);
assert_eq!(
Color::from(LinSrgb::new(0.5f32, 0.5, 0.5)),
Color::Rgb(188, 188, 188)
);
}
}

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![
@@ -133,17 +208,13 @@ macro_rules! modifier {
/// "world".green().on_yellow().not_bold(),
/// ]);
/// let paragraph = Paragraph::new(line).italic().underlined();
/// let block = Block::default()
/// .title("Title")
/// .borders(Borders::ALL)
/// .on_white()
/// .bold();
/// let block = Block::bordered().title("Title").on_white().bold();
/// ```
pub trait Stylize<'a, T>: Sized {
#[must_use = "`bg` returns the modified style without modifying the original"]
fn bg(self, color: Color) -> T;
fn bg<C: Into<Color>>(self, color: C) -> T;
#[must_use = "`fg` returns the modified style without modifying the original"]
fn fg<S: Into<Color>>(self, color: S) -> T;
fn fg<C: Into<Color>>(self, color: C) -> T;
#[must_use = "`reset` returns the modified style without modifying the original"]
fn reset(self) -> T;
#[must_use = "`add_modifier` returns the modified style without modifying the original"]
@@ -183,12 +254,12 @@ impl<'a, T, U> Stylize<'a, T> for U
where
U: Styled<Item = T>,
{
fn bg(self, color: Color) -> T {
let style = self.style().bg(color);
fn bg<C: Into<Color>>(self, color: C) -> T {
let style = self.style().bg(color.into());
self.set_style(style)
}
fn fg<S: Into<Color>>(self, color: S) -> T {
fn fg<C: Into<Color>>(self, color: C) -> T {
let style = self.style().fg(color.into());
self.set_style(style)
}
@@ -235,6 +306,7 @@ impl Styled for String {
#[cfg(test)]
mod tests {
use itertools::Itertools;
use rstest::rstest;
use super::*;
@@ -348,9 +420,9 @@ mod tests {
// format!() is used to create a temporary String inside a closure, which suffers the same
// issue as above without the `Styled` trait impl for `String`
let items = vec![String::from("a"), String::from("b")];
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]
@@ -393,12 +465,12 @@ mod tests {
#[test]
fn repeated_attributes() {
let cyan_bg = Style::default().bg(Color::Cyan);
let cyan_fg = Style::default().fg(Color::Cyan);
let bg = Style::default().bg(Color::Cyan);
let fg = Style::default().fg(Color::Cyan);
// Behavior: the last one set is the definitive one
assert_eq!("hello".on_red().on_cyan(), Span::styled("hello", cyan_bg));
assert_eq!("hello".red().cyan(), Span::styled("hello", cyan_fg));
assert_eq!("hello".on_red().on_cyan(), Span::styled("hello", bg));
assert_eq!("hello".red().cyan(), Span::styled("hello", fg));
}
#[test]
@@ -427,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);
}
}

238
ratatui-core/src/symbols.rs Normal file
View File

@@ -0,0 +1,238 @@
//! Symbols and markers for drawing various widgets.
use strum::{Display, EnumString};
pub mod border;
pub mod line;
pub mod block {
pub const FULL: &str = "";
pub const SEVEN_EIGHTHS: &str = "";
pub const THREE_QUARTERS: &str = "";
pub const FIVE_EIGHTHS: &str = "";
pub const HALF: &str = "";
pub const THREE_EIGHTHS: &str = "";
pub const ONE_QUARTER: &str = "";
pub const ONE_EIGHTH: &str = "";
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Set {
pub full: &'static str,
pub seven_eighths: &'static str,
pub three_quarters: &'static str,
pub five_eighths: &'static str,
pub half: &'static str,
pub three_eighths: &'static str,
pub one_quarter: &'static str,
pub one_eighth: &'static str,
pub empty: &'static str,
}
impl Default for Set {
fn default() -> Self {
NINE_LEVELS
}
}
pub const THREE_LEVELS: Set = Set {
full: FULL,
seven_eighths: FULL,
three_quarters: HALF,
five_eighths: HALF,
half: HALF,
three_eighths: HALF,
one_quarter: HALF,
one_eighth: " ",
empty: " ",
};
pub const NINE_LEVELS: Set = Set {
full: FULL,
seven_eighths: SEVEN_EIGHTHS,
three_quarters: THREE_QUARTERS,
five_eighths: FIVE_EIGHTHS,
half: HALF,
three_eighths: THREE_EIGHTHS,
one_quarter: ONE_QUARTER,
one_eighth: ONE_EIGHTH,
empty: " ",
};
}
pub mod half_block {
pub const UPPER: char = '▀';
pub const LOWER: char = '▄';
pub const FULL: char = '█';
}
pub mod bar {
pub const FULL: &str = "";
pub const SEVEN_EIGHTHS: &str = "";
pub const THREE_QUARTERS: &str = "";
pub const FIVE_EIGHTHS: &str = "";
pub const HALF: &str = "";
pub const THREE_EIGHTHS: &str = "";
pub const ONE_QUARTER: &str = "";
pub const ONE_EIGHTH: &str = "";
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Set {
pub full: &'static str,
pub seven_eighths: &'static str,
pub three_quarters: &'static str,
pub five_eighths: &'static str,
pub half: &'static str,
pub three_eighths: &'static str,
pub one_quarter: &'static str,
pub one_eighth: &'static str,
pub empty: &'static str,
}
impl Default for Set {
fn default() -> Self {
NINE_LEVELS
}
}
pub const THREE_LEVELS: Set = Set {
full: FULL,
seven_eighths: FULL,
three_quarters: HALF,
five_eighths: HALF,
half: HALF,
three_eighths: HALF,
one_quarter: HALF,
one_eighth: " ",
empty: " ",
};
pub const NINE_LEVELS: Set = Set {
full: FULL,
seven_eighths: SEVEN_EIGHTHS,
three_quarters: THREE_QUARTERS,
five_eighths: FIVE_EIGHTHS,
half: HALF,
three_eighths: THREE_EIGHTHS,
one_quarter: ONE_QUARTER,
one_eighth: ONE_EIGHTH,
empty: " ",
};
}
pub const DOT: &str = "";
pub mod braille {
pub const BLANK: u16 = 0x2800;
pub const DOTS: [[u16; 2]; 4] = [
[0x0001, 0x0008],
[0x0002, 0x0010],
[0x0004, 0x0020],
[0x0040, 0x0080],
];
}
/// Marker to use when plotting data points
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Marker {
/// One point per cell in shape of dot (`•`)
#[default]
Dot,
/// One point per cell in shape of a block (`█`)
Block,
/// One point per cell in the shape of a bar (`▄`)
Bar,
/// Use the [Unicode Braille Patterns](https://en.wikipedia.org/wiki/Braille_Patterns) block to
/// represent data points.
///
/// This is a 2x4 grid of dots, where each dot can be either on or off.
///
/// Note: Support for this marker is limited to terminals and fonts that support Unicode
/// Braille Patterns. If your terminal does not support this, you will see unicode replacement
/// characters (`<60>`) instead of Braille dots (`⠓`, `⣇`, `⣿`).
Braille,
/// Use the unicode block and half block characters (`█`, `▄`, and `▀`) to represent points in
/// a grid that is double the resolution of the terminal. Because each terminal cell is
/// generally about twice as tall as it is wide, this allows for a square grid of pixels.
HalfBlock,
}
pub mod scrollbar {
use crate::symbols::{block, line};
/// Scrollbar Set
/// ```text
/// <--▮------->
/// ^ ^ ^ ^
/// │ │ │ └ end
/// │ │ └──── track
/// │ └──────── thumb
/// └─────────── begin
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Set {
pub track: &'static str,
pub thumb: &'static str,
pub begin: &'static str,
pub end: &'static str,
}
pub const DOUBLE_VERTICAL: Set = Set {
track: line::DOUBLE_VERTICAL,
thumb: block::FULL,
begin: "",
end: "",
};
pub const DOUBLE_HORIZONTAL: Set = Set {
track: line::DOUBLE_HORIZONTAL,
thumb: block::FULL,
begin: "",
end: "",
};
pub const VERTICAL: Set = Set {
track: line::VERTICAL,
thumb: block::FULL,
begin: "",
end: "",
};
pub const HORIZONTAL: Set = Set {
track: line::HORIZONTAL,
thumb: block::FULL,
begin: "",
end: "",
};
}
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;
use super::*;
#[test]
fn marker_tostring() {
assert_eq!(Marker::Dot.to_string(), "Dot");
assert_eq!(Marker::Block.to_string(), "Block");
assert_eq!(Marker::Bar.to_string(), "Bar");
assert_eq!(Marker::Braille.to_string(), "Braille");
}
#[test]
fn marker_from_str() {
assert_eq!("Dot".parse::<Marker>(), Ok(Marker::Dot));
assert_eq!("Block".parse::<Marker>(), Ok(Marker::Block));
assert_eq!("Bar".parse::<Marker>(), Ok(Marker::Bar));
assert_eq!("Braille".parse::<Marker>(), Ok(Marker::Braille));
assert_eq!("".parse::<Marker>(), Err(ParseError::VariantNotFound));
}
}

View File

@@ -0,0 +1,509 @@
use crate::symbols::{block, line};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Set {
pub top_left: &'static str,
pub top_right: &'static str,
pub bottom_left: &'static str,
pub bottom_right: &'static str,
pub vertical_left: &'static str,
pub vertical_right: &'static str,
pub horizontal_top: &'static str,
pub horizontal_bottom: &'static str,
}
impl Default for Set {
fn default() -> Self {
PLAIN
}
}
/// Border Set with a single line width
///
/// ```text
/// ┌─────┐
/// │xxxxx│
/// │xxxxx│
/// └─────┘
/// ```
pub const PLAIN: Set = Set {
top_left: line::NORMAL.top_left,
top_right: line::NORMAL.top_right,
bottom_left: line::NORMAL.bottom_left,
bottom_right: line::NORMAL.bottom_right,
vertical_left: line::NORMAL.vertical,
vertical_right: line::NORMAL.vertical,
horizontal_top: line::NORMAL.horizontal,
horizontal_bottom: line::NORMAL.horizontal,
};
/// Border Set with a single line width and rounded corners
///
/// ```text
/// ╭─────╮
/// │xxxxx│
/// │xxxxx│
/// ╰─────╯
/// ```
pub const ROUNDED: Set = Set {
top_left: line::ROUNDED.top_left,
top_right: line::ROUNDED.top_right,
bottom_left: line::ROUNDED.bottom_left,
bottom_right: line::ROUNDED.bottom_right,
vertical_left: line::ROUNDED.vertical,
vertical_right: line::ROUNDED.vertical,
horizontal_top: line::ROUNDED.horizontal,
horizontal_bottom: line::ROUNDED.horizontal,
};
/// Border Set with a double line width
///
/// ```text
/// ╔═════╗
/// ║xxxxx║
/// ║xxxxx║
/// ╚═════╝
/// ```
pub const DOUBLE: Set = Set {
top_left: line::DOUBLE.top_left,
top_right: line::DOUBLE.top_right,
bottom_left: line::DOUBLE.bottom_left,
bottom_right: line::DOUBLE.bottom_right,
vertical_left: line::DOUBLE.vertical,
vertical_right: line::DOUBLE.vertical,
horizontal_top: line::DOUBLE.horizontal,
horizontal_bottom: line::DOUBLE.horizontal,
};
/// Border Set with a thick line width
///
/// ```text
/// ┏━━━━━┓
/// ┃xxxxx┃
/// ┃xxxxx┃
/// ┗━━━━━┛
/// ```
pub const THICK: Set = Set {
top_left: line::THICK.top_left,
top_right: line::THICK.top_right,
bottom_left: line::THICK.bottom_left,
bottom_right: line::THICK.bottom_right,
vertical_left: line::THICK.vertical,
vertical_right: line::THICK.vertical,
horizontal_top: line::THICK.horizontal,
horizontal_bottom: line::THICK.horizontal,
};
pub const QUADRANT_TOP_LEFT: &str = "";
pub const QUADRANT_TOP_RIGHT: &str = "";
pub const QUADRANT_BOTTOM_LEFT: &str = "";
pub const QUADRANT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_HALF: &str = "";
pub const QUADRANT_BOTTOM_HALF: &str = "";
pub const QUADRANT_LEFT_HALF: &str = "";
pub const QUADRANT_RIGHT_HALF: &str = "";
pub const QUADRANT_TOP_LEFT_BOTTOM_LEFT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT: &str = "";
pub const QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_RIGHT_BOTTOM_LEFT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_LEFT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_RIGHT_BOTTOM_LEFT: &str = "";
pub const QUADRANT_BLOCK: &str = "";
/// Quadrant used for setting a border outside a block by one half cell "pixel".
///
/// ```text
/// ▛▀▀▀▀▀▜
/// ▌xxxxx▐
/// ▌xxxxx▐
/// ▙▄▄▄▄▄▟
/// ```
pub const QUADRANT_OUTSIDE: Set = Set {
top_left: QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT,
top_right: QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT,
bottom_left: QUADRANT_TOP_LEFT_BOTTOM_LEFT_BOTTOM_RIGHT,
bottom_right: QUADRANT_TOP_RIGHT_BOTTOM_LEFT_BOTTOM_RIGHT,
vertical_left: QUADRANT_LEFT_HALF,
vertical_right: QUADRANT_RIGHT_HALF,
horizontal_top: QUADRANT_TOP_HALF,
horizontal_bottom: QUADRANT_BOTTOM_HALF,
};
/// Quadrant used for setting a border inside a block by one half cell "pixel".
///
/// ```text
/// ▗▄▄▄▄▄▖
/// ▐xxxxx▌
/// ▐xxxxx▌
/// ▝▀▀▀▀▀▘
/// ```
pub const QUADRANT_INSIDE: Set = Set {
top_right: QUADRANT_BOTTOM_LEFT,
top_left: QUADRANT_BOTTOM_RIGHT,
bottom_right: QUADRANT_TOP_LEFT,
bottom_left: QUADRANT_TOP_RIGHT,
vertical_left: QUADRANT_RIGHT_HALF,
vertical_right: QUADRANT_LEFT_HALF,
horizontal_top: QUADRANT_BOTTOM_HALF,
horizontal_bottom: QUADRANT_TOP_HALF,
};
pub const ONE_EIGHTH_TOP_EIGHT: &str = "";
pub const ONE_EIGHTH_BOTTOM_EIGHT: &str = "";
pub const ONE_EIGHTH_LEFT_EIGHT: &str = "";
pub const ONE_EIGHTH_RIGHT_EIGHT: &str = "";
/// Wide border set based on McGugan box technique
///
/// ```text
/// ▁▁▁▁▁▁▁
/// ▏xxxxx▕
/// ▏xxxxx▕
/// ▔▔▔▔▔▔▔
/// ```
#[allow(clippy::doc_markdown)]
pub const ONE_EIGHTH_WIDE: Set = Set {
top_right: ONE_EIGHTH_BOTTOM_EIGHT,
top_left: ONE_EIGHTH_BOTTOM_EIGHT,
bottom_right: ONE_EIGHTH_TOP_EIGHT,
bottom_left: ONE_EIGHTH_TOP_EIGHT,
vertical_left: ONE_EIGHTH_LEFT_EIGHT,
vertical_right: ONE_EIGHTH_RIGHT_EIGHT,
horizontal_top: ONE_EIGHTH_BOTTOM_EIGHT,
horizontal_bottom: ONE_EIGHTH_TOP_EIGHT,
};
/// Tall border set based on McGugan box technique
///
/// ```text
/// ▕▔▔▏
/// ▕xx▏
/// ▕xx▏
/// ▕▁▁▏
/// ```
#[allow(clippy::doc_markdown)]
pub const ONE_EIGHTH_TALL: Set = Set {
top_right: ONE_EIGHTH_LEFT_EIGHT,
top_left: ONE_EIGHTH_RIGHT_EIGHT,
bottom_right: ONE_EIGHTH_LEFT_EIGHT,
bottom_left: ONE_EIGHTH_RIGHT_EIGHT,
vertical_left: ONE_EIGHTH_RIGHT_EIGHT,
vertical_right: ONE_EIGHTH_LEFT_EIGHT,
horizontal_top: ONE_EIGHTH_TOP_EIGHT,
horizontal_bottom: ONE_EIGHTH_BOTTOM_EIGHT,
};
/// Wide proportional (visually equal width and height) border with using set of quadrants.
///
/// The border is created by using half blocks for top and bottom, and full
/// blocks for right and left sides to make horizontal and vertical borders seem equal.
///
/// ```text
/// ▄▄▄▄
/// █xx█
/// █xx█
/// ▀▀▀▀
/// ```
pub const PROPORTIONAL_WIDE: Set = Set {
top_right: QUADRANT_BOTTOM_HALF,
top_left: QUADRANT_BOTTOM_HALF,
bottom_right: QUADRANT_TOP_HALF,
bottom_left: QUADRANT_TOP_HALF,
vertical_left: QUADRANT_BLOCK,
vertical_right: QUADRANT_BLOCK,
horizontal_top: QUADRANT_BOTTOM_HALF,
horizontal_bottom: QUADRANT_TOP_HALF,
};
/// Tall proportional (visually equal width and height) border with using set of quadrants.
///
/// The border is created by using full blocks for all sides, except for the top and bottom,
/// which use half blocks to make horizontal and vertical borders seem equal.
///
/// ```text
/// ▕█▀▀█
/// ▕█xx█
/// ▕█xx█
/// ▕█▄▄█
/// ```
pub const PROPORTIONAL_TALL: Set = Set {
top_right: QUADRANT_BLOCK,
top_left: QUADRANT_BLOCK,
bottom_right: QUADRANT_BLOCK,
bottom_left: QUADRANT_BLOCK,
vertical_left: QUADRANT_BLOCK,
vertical_right: QUADRANT_BLOCK,
horizontal_top: QUADRANT_TOP_HALF,
horizontal_bottom: QUADRANT_BOTTOM_HALF,
};
/// Solid border set
///
/// The border is created by using full blocks for all sides.
///
/// ```text
/// ████
/// █xx█
/// █xx█
/// ████
/// ```
pub const FULL: Set = Set {
top_left: block::FULL,
top_right: block::FULL,
bottom_left: block::FULL,
bottom_right: block::FULL,
vertical_left: block::FULL,
vertical_right: block::FULL,
horizontal_top: block::FULL,
horizontal_bottom: block::FULL,
};
/// Empty border set
///
/// The border is created by using empty strings for all sides.
///
/// This is useful for ensuring that the border style is applied to a border on a block with a title
/// without actually drawing a border.
///
/// ░ Example
///
/// `░` represents the content in the area not covered by the border to make it easier to see the
/// blank symbols.
///
/// ```text
/// ░░░░░░░░
/// ░░ ░░
/// ░░ ░░ ░░
/// ░░ ░░ ░░
/// ░░ ░░
/// ░░░░░░░░
/// ```
pub const EMPTY: Set = Set {
top_left: " ",
top_right: " ",
bottom_left: " ",
bottom_right: " ",
vertical_left: " ",
vertical_right: " ",
horizontal_top: " ",
horizontal_bottom: " ",
};
#[cfg(test)]
mod tests {
use indoc::{formatdoc, indoc};
use super::*;
#[test]
fn default() {
assert_eq!(Set::default(), PLAIN);
}
/// A helper function to render a border set to a string.
///
/// '░' (U+2591 Light Shade) is used as a placeholder for empty space to make it easier to see
/// the size of the border symbols.
fn render(set: Set) -> String {
formatdoc!(
"░░░░░░
░{}{}{}{}░
░{}░░{}░
░{}░░{}░
░{}{}{}{}░
░░░░░░",
set.top_left,
set.horizontal_top,
set.horizontal_top,
set.top_right,
set.vertical_left,
set.vertical_right,
set.vertical_left,
set.vertical_right,
set.bottom_left,
set.horizontal_bottom,
set.horizontal_bottom,
set.bottom_right
)
}
#[test]
fn plain() {
assert_eq!(
render(PLAIN),
indoc!(
"░░░░░░
░┌──┐░
░│░░│░
░│░░│░
░└──┘░
░░░░░░"
)
);
}
#[test]
fn rounded() {
assert_eq!(
render(ROUNDED),
indoc!(
"░░░░░░
░╭──╮░
░│░░│░
░│░░│░
░╰──╯░
░░░░░░"
)
);
}
#[test]
fn double() {
assert_eq!(
render(DOUBLE),
indoc!(
"░░░░░░
░╔══╗░
░║░░║░
░║░░║░
░╚══╝░
░░░░░░"
)
);
}
#[test]
fn thick() {
assert_eq!(
render(THICK),
indoc!(
"░░░░░░
░┏━━┓░
░┃░░┃░
░┃░░┃░
░┗━━┛░
░░░░░░"
)
);
}
#[test]
fn quadrant_outside() {
assert_eq!(
render(QUADRANT_OUTSIDE),
indoc!(
"░░░░░░
░▛▀▀▜░
░▌░░▐░
░▌░░▐░
░▙▄▄▟░
░░░░░░"
)
);
}
#[test]
fn quadrant_inside() {
assert_eq!(
render(QUADRANT_INSIDE),
indoc!(
"░░░░░░
░▗▄▄▖░
░▐░░▌░
░▐░░▌░
░▝▀▀▘░
░░░░░░"
)
);
}
#[test]
fn one_eighth_wide() {
assert_eq!(
render(ONE_EIGHTH_WIDE),
indoc!(
"░░░░░░
░▁▁▁▁░
░▏░░▕░
░▏░░▕░
░▔▔▔▔░
░░░░░░"
)
);
}
#[test]
fn one_eighth_tall() {
assert_eq!(
render(ONE_EIGHTH_TALL),
indoc!(
"░░░░░░
░▕▔▔▏░
░▕░░▏░
░▕░░▏░
░▕▁▁▏░
░░░░░░"
)
);
}
#[test]
fn proportional_wide() {
assert_eq!(
render(PROPORTIONAL_WIDE),
indoc!(
"░░░░░░
░▄▄▄▄░
░█░░█░
░█░░█░
░▀▀▀▀░
░░░░░░"
)
);
}
#[test]
fn proportional_tall() {
assert_eq!(
render(PROPORTIONAL_TALL),
indoc!(
"░░░░░░
░█▀▀█░
░█░░█░
░█░░█░
░█▄▄█░
░░░░░░"
)
);
}
#[test]
fn full() {
assert_eq!(
render(FULL),
indoc!(
"░░░░░░
░████░
░█░░█░
░█░░█░
░████░
░░░░░░"
)
);
}
#[test]
fn empty() {
assert_eq!(
render(EMPTY),
indoc!(
"░░░░░░
░ ░
░ ░░ ░
░ ░░ ░
░ ░
░░░░░░"
)
);
}
}

View File

@@ -0,0 +1,208 @@
pub const VERTICAL: &str = "";
pub const DOUBLE_VERTICAL: &str = "";
pub const THICK_VERTICAL: &str = "";
pub const HORIZONTAL: &str = "";
pub const DOUBLE_HORIZONTAL: &str = "";
pub const THICK_HORIZONTAL: &str = "";
pub const TOP_RIGHT: &str = "";
pub const ROUNDED_TOP_RIGHT: &str = "";
pub const DOUBLE_TOP_RIGHT: &str = "";
pub const THICK_TOP_RIGHT: &str = "";
pub const TOP_LEFT: &str = "";
pub const ROUNDED_TOP_LEFT: &str = "";
pub const DOUBLE_TOP_LEFT: &str = "";
pub const THICK_TOP_LEFT: &str = "";
pub const BOTTOM_RIGHT: &str = "";
pub const ROUNDED_BOTTOM_RIGHT: &str = "";
pub const DOUBLE_BOTTOM_RIGHT: &str = "";
pub const THICK_BOTTOM_RIGHT: &str = "";
pub const BOTTOM_LEFT: &str = "";
pub const ROUNDED_BOTTOM_LEFT: &str = "";
pub const DOUBLE_BOTTOM_LEFT: &str = "";
pub const THICK_BOTTOM_LEFT: &str = "";
pub const VERTICAL_LEFT: &str = "";
pub const DOUBLE_VERTICAL_LEFT: &str = "";
pub const THICK_VERTICAL_LEFT: &str = "";
pub const VERTICAL_RIGHT: &str = "";
pub const DOUBLE_VERTICAL_RIGHT: &str = "";
pub const THICK_VERTICAL_RIGHT: &str = "";
pub const HORIZONTAL_DOWN: &str = "";
pub const DOUBLE_HORIZONTAL_DOWN: &str = "";
pub const THICK_HORIZONTAL_DOWN: &str = "";
pub const HORIZONTAL_UP: &str = "";
pub const DOUBLE_HORIZONTAL_UP: &str = "";
pub const THICK_HORIZONTAL_UP: &str = "";
pub const CROSS: &str = "";
pub const DOUBLE_CROSS: &str = "";
pub const THICK_CROSS: &str = "";
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Set {
pub vertical: &'static str,
pub horizontal: &'static str,
pub top_right: &'static str,
pub top_left: &'static str,
pub bottom_right: &'static str,
pub bottom_left: &'static str,
pub vertical_left: &'static str,
pub vertical_right: &'static str,
pub horizontal_down: &'static str,
pub horizontal_up: &'static str,
pub cross: &'static str,
}
impl Default for Set {
fn default() -> Self {
NORMAL
}
}
pub const NORMAL: Set = Set {
vertical: VERTICAL,
horizontal: HORIZONTAL,
top_right: TOP_RIGHT,
top_left: TOP_LEFT,
bottom_right: BOTTOM_RIGHT,
bottom_left: BOTTOM_LEFT,
vertical_left: VERTICAL_LEFT,
vertical_right: VERTICAL_RIGHT,
horizontal_down: HORIZONTAL_DOWN,
horizontal_up: HORIZONTAL_UP,
cross: CROSS,
};
pub const ROUNDED: Set = Set {
top_right: ROUNDED_TOP_RIGHT,
top_left: ROUNDED_TOP_LEFT,
bottom_right: ROUNDED_BOTTOM_RIGHT,
bottom_left: ROUNDED_BOTTOM_LEFT,
..NORMAL
};
pub const DOUBLE: Set = Set {
vertical: DOUBLE_VERTICAL,
horizontal: DOUBLE_HORIZONTAL,
top_right: DOUBLE_TOP_RIGHT,
top_left: DOUBLE_TOP_LEFT,
bottom_right: DOUBLE_BOTTOM_RIGHT,
bottom_left: DOUBLE_BOTTOM_LEFT,
vertical_left: DOUBLE_VERTICAL_LEFT,
vertical_right: DOUBLE_VERTICAL_RIGHT,
horizontal_down: DOUBLE_HORIZONTAL_DOWN,
horizontal_up: DOUBLE_HORIZONTAL_UP,
cross: DOUBLE_CROSS,
};
pub const THICK: Set = Set {
vertical: THICK_VERTICAL,
horizontal: THICK_HORIZONTAL,
top_right: THICK_TOP_RIGHT,
top_left: THICK_TOP_LEFT,
bottom_right: THICK_BOTTOM_RIGHT,
bottom_left: THICK_BOTTOM_LEFT,
vertical_left: THICK_VERTICAL_LEFT,
vertical_right: THICK_VERTICAL_RIGHT,
horizontal_down: THICK_HORIZONTAL_DOWN,
horizontal_up: THICK_HORIZONTAL_UP,
cross: THICK_CROSS,
};
#[cfg(test)]
mod tests {
use indoc::{formatdoc, indoc};
use super::*;
#[test]
fn default() {
assert_eq!(Set::default(), NORMAL);
}
/// A helper function to render a set of symbols.
fn render(set: Set) -> String {
formatdoc!(
"{}{}{}{}
{}{}{}{}
{}{}{}{}
{}{}{}{}",
set.top_left,
set.horizontal,
set.horizontal_down,
set.top_right,
set.vertical,
" ",
set.vertical,
set.vertical,
set.vertical_right,
set.horizontal,
set.cross,
set.vertical_left,
set.bottom_left,
set.horizontal,
set.horizontal_up,
set.bottom_right
)
}
#[test]
fn normal() {
assert_eq!(
render(NORMAL),
indoc!(
"┌─┬┐
│ ││
├─┼┤
└─┴┘"
)
);
}
#[test]
fn rounded() {
assert_eq!(
render(ROUNDED),
indoc!(
"╭─┬╮
│ ││
├─┼┤
╰─┴╯"
)
);
}
#[test]
fn double() {
assert_eq!(
render(DOUBLE),
indoc!(
"╔═╦╗
║ ║║
╠═╬╣
╚═╩╝"
)
);
}
#[test]
fn thick() {
assert_eq!(
render(THICK),
indoc!(
"┏━┳┓
┃ ┃┃
┣━╋┫
┗━┻┛"
)
);
}
}

View File

@@ -1,45 +1,48 @@
//! 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`].
//! [`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![
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
//! // ])
//! let block = Block::default().title("My title");
//! let block = Block::new().title("My title");
//!
//! // A simple string with a unique style.
//! // Converted to Line(vec![
//! // Span { content: Cow::Borrowed("My title"), style: Style { fg: Some(Color::Yellow), .. }
//! // ])
//! let block =
//! Block::default().title(Span::styled("My title", Style::default().fg(Color::Yellow)));
//! let block = Block::new().title(Span::styled("My title", Style::default().fg(Color::Yellow)));
//!
//! // A string with multiple styles.
//! // Converted to Line(vec![
//! // Span { content: Cow::Borrowed("My"), style: Style { fg: Some(Color::Yellow), .. } },
//! // Span { content: Cow::Borrowed(" title"), .. }
//! // ])
//! let block = Block::default().title(vec![
//! let block = Block::new().title(vec![
//! Span::styled("My", Style::default().fg(Color::Yellow)),
//! Span::raw(" title"),
//! ]);
@@ -49,14 +52,13 @@ mod grapheme;
pub use grapheme::StyledGrapheme;
mod line;
pub use line::Line;
pub use line::{Line, ToLine};
mod masked;
pub use masked::Masked;
mod span;
pub use span::Span;
pub use span::{Span, ToSpan};
#[allow(clippy::module_inception)]
mod text;
pub use text::Text;
pub use text::{Text, ToText};

View File

@@ -1,4 +1,7 @@
use crate::prelude::*;
use crate::style::{Style, Styled};
const NBSP: &str = "\u{00a0}";
const ZWSP: &str = "\u{200b}";
/// A grapheme associated to a style.
/// Note that, although `StyledGrapheme` is the smallest divisible unit of text,
@@ -16,16 +19,23 @@ 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>`]).
pub fn new<S: Into<Style>>(symbol: &'a str, style: S) -> StyledGrapheme<'a> {
StyledGrapheme {
///
/// [`Color`]: crate::style::Color
pub fn new<S: Into<Style>>(symbol: &'a str, style: S) -> Self {
Self {
symbol,
style: style.into(),
}
}
pub fn is_whitespace(&self) -> bool {
let symbol = self.symbol;
symbol == ZWSP || symbol.chars().all(char::is_whitespace) && symbol != NBSP
}
}
impl<'a> Styled for StyledGrapheme<'a> {
type Item = StyledGrapheme<'a>;
type Item = Self;
fn style(&self) -> Style {
self.style
@@ -40,6 +50,7 @@ impl<'a> Styled for StyledGrapheme<'a> {
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Stylize;
#[test]
fn new() {

1671
ratatui-core/src/text/line.rs Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,27 @@
use std::{
borrow::Cow,
fmt::{self, Debug, Display},
};
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);
/// assert_eq!(buffer, Buffer::with_lines(vec!["xxxxx"]));
/// Text::from(password).render(buffer.area, &mut buffer);
/// assert_eq!(buffer, Buffer::with_lines(["xxxxx"]));
/// ```
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct Masked<'a> {
@@ -36,7 +38,7 @@ impl<'a> Masked<'a> {
}
/// The character to use for masking.
pub fn mask_char(&self) -> char {
pub const fn mask_char(&self) -> char {
self.mask_char
}
@@ -46,40 +48,41 @@ impl<'a> Masked<'a> {
}
}
impl Debug for Masked<'_> {
impl fmt::Debug for Masked<'_> {
/// Debug representation of a masked string is the underlying string
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.inner).map_err(|_| fmt::Error)
// note that calling display instead of Debug here is intentional
fmt::Display::fmt(&self.inner, f)
}
}
impl Display for Masked<'_> {
impl fmt::Display for Masked<'_> {
/// Display representation of a masked string is the masked string
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.value()).map_err(|_| fmt::Error)
fmt::Display::fmt(&self.value(), f)
}
}
impl<'a> From<&'a Masked<'a>> for Cow<'a, str> {
fn from(masked: &'a Masked) -> Cow<'a, str> {
fn from(masked: &'a Masked) -> Self {
masked.value()
}
}
impl<'a> From<Masked<'a>> for Cow<'a, str> {
fn from(masked: Masked<'a>) -> Cow<'a, str> {
fn from(masked: Masked<'a>) -> Self {
masked.value()
}
}
impl<'a> From<&'a Masked<'_>> for Text<'a> {
fn from(masked: &'a Masked) -> Text<'a> {
fn from(masked: &'a Masked) -> Self {
Text::raw(masked.value())
}
}
impl<'a> From<Masked<'a>> for Text<'a> {
fn from(masked: Masked<'a>) -> Text<'a> {
fn from(masked: Masked<'a>) -> Self {
Text::raw(masked.value())
}
}
@@ -112,12 +115,14 @@ mod tests {
fn debug() {
let masked = Masked::new("12345", 'x');
assert_eq!(format!("{masked:?}"), "12345");
assert_eq!(format!("{masked:.3?}"), "123", "Debug truncates");
}
#[test]
fn display() {
let masked = Masked::new("12345", 'x');
assert_eq!(format!("{masked}"), "xxxxx");
assert_eq!(format!("{masked:.3}"), "xxx", "Display truncates");
}
#[test]
@@ -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

@@ -1,10 +1,15 @@
use std::{borrow::Cow, fmt::Debug};
use std::{borrow::Cow, fmt};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use super::StyledGrapheme;
use crate::{prelude::*, widgets::Widget};
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.
///
@@ -37,7 +42,7 @@ use crate::{prelude::*, widgets::Widget};
/// 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"));
@@ -51,7 +56,10 @@ use crate::{prelude::*, widgets::Widget};
/// 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());
@@ -65,7 +73,7 @@ use crate::{prelude::*, widgets::Widget};
/// 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"))
@@ -74,27 +82,40 @@ use crate::{prelude::*, widgets::Widget};
/// .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> {
@@ -103,15 +124,16 @@ impl<'a> Span<'a> {
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// use ratatui_core::text::Span;
///
/// Span::raw("test content");
/// Span::raw(String::from("test content"));
/// ```
pub fn raw<T>(content: T) -> Span<'a>
pub fn raw<T>(content: T) -> Self
where
T: Into<Cow<'a, str>>,
{
Span {
Self {
content: content.into(),
style: Style::default(),
}
@@ -128,17 +150,23 @@ 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);
/// ```
pub fn styled<T, S>(content: T, style: S) -> Span<'a>
///
/// [`Color`]: crate::style::Color
pub fn styled<T, S>(content: T, style: S) -> Self
where
T: Into<Cow<'a, str>>,
S: Into<Style>,
{
Span {
Self {
content: content.into(),
style: style.into(),
}
@@ -154,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"]
@@ -179,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();
@@ -198,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);
@@ -218,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(),
@@ -249,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();
@@ -264,6 +312,8 @@ impl<'a> Span<'a> {
/// ],
/// );
/// ```
///
/// [`Color`]: crate::style::Color
pub fn styled_graphemes<S: Into<Style>>(
&'a self,
base_style: S,
@@ -281,37 +331,60 @@ impl<'a> Span<'a> {
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// let l = "Test Content".green().italic().to_left_aligned_line();
/// 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"]
pub fn to_left_aligned_line(self) -> Line<'a> {
pub fn into_left_aligned_line(self) -> Line<'a> {
Line::from(self).left_aligned()
}
#[allow(clippy::wrong_self_convention)]
#[deprecated = "use into_left_aligned_line"]
pub fn to_left_aligned_line(self) -> Line<'a> {
self.into_left_aligned_line()
}
/// Converts this Span into a center-aligned [`Line`]
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// let l = "Test Content".green().italic().to_centered_line();
/// use ratatui_core::style::Stylize;
///
/// let line = "Test Content".green().italic().into_centered_line();
/// ```
pub fn to_centered_line(self) -> Line<'a> {
#[must_use = "method moves the value of self and returns the modified value"]
pub fn into_centered_line(self) -> Line<'a> {
Line::from(self).centered()
}
#[allow(clippy::wrong_self_convention)]
#[deprecated = "use into_centered_line"]
pub fn to_centered_line(self) -> Line<'a> {
self.into_centered_line()
}
/// Converts this Span into a right-aligned [`Line`]
///
/// # Example
///
/// ```rust
/// # use ratatui::prelude::*;
/// let l = "Test Content".green().italic().to_right_aligned_line();
/// use ratatui_core::style::Stylize;
///
/// let line = "Test Content".green().italic().into_right_aligned_line();
/// ```
pub fn to_right_aligned_line(self) -> Line<'a> {
#[must_use = "method moves the value of self and returns the modified value"]
pub fn into_right_aligned_line(self) -> Line<'a> {
Line::from(self).right_aligned()
}
#[allow(clippy::wrong_self_convention)]
#[deprecated = "use into_right_aligned_line"]
pub fn to_right_aligned_line(self) -> Line<'a> {
self.into_right_aligned_line()
}
}
impl<'a, T> From<T> for Span<'a>
@@ -323,8 +396,16 @@ where
}
}
impl<'a> std::ops::Add<Self> for Span<'a> {
type Output = Line<'a>;
fn add(self, rhs: Self) -> Self::Output {
Line::from_iter([self, rhs])
}
}
impl<'a> Styled for Span<'a> {
type Item = Span<'a>;
type Item = Self;
fn style(&self) -> Style {
self.style
@@ -337,52 +418,103 @@ 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) {
let Rect {
x: mut current_x,
y,
width,
..
} = area;
let max_x = Ord::min(current_x.saturating_add(width), buf.area.right());
for g in self.styled_graphemes(Style::default()) {
let symbol_width = g.symbol.width();
let next_x = current_x.saturating_add(symbol_width as u16);
if next_x > max_x {
impl Widget for &Span<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let area = area.intersection(buf.area);
if area.is_empty() {
return;
}
let Rect { mut x, y, .. } = area;
for (i, grapheme) in self.styled_graphemes(Style::default()).enumerate() {
let symbol_width = grapheme.symbol.width();
let next_x = x.saturating_add(symbol_width as u16);
if next_x > area.right() {
break;
}
buf.get_mut(current_x, y)
.set_symbol(g.symbol)
.set_style(g.style);
if i == 0 {
// the first grapheme is always set on the cell
buf[(x, y)]
.set_symbol(grapheme.symbol)
.set_style(grapheme.style);
} else if x == area.x {
// there is one or more zero-width graphemes in the first cell, so the first cell
// must be appended to.
buf[(x, y)]
.append_symbol(grapheme.symbol)
.set_style(grapheme.style);
} else if symbol_width == 0 {
// append zero-width graphemes to the previous cell
buf[(x - 1, y)]
.append_symbol(grapheme.symbol)
.set_style(grapheme.style);
} else {
// just a normal grapheme (not first, not zero-width, not overflowing the area)
buf[(x, y)]
.set_symbol(grapheme.symbol)
.set_style(grapheme.style);
}
// multi-width graphemes must clear the cells of characters that are hidden by the
// grapheme, otherwise the hidden characters will be re-rendered if the grapheme is
// overwritten.
for i in (current_x + 1)..next_x {
buf.get_mut(i, y).reset();
for x_hidden in (x + 1)..next_x {
// it may seem odd that the style of the hidden cells are not set to the style of
// the grapheme, but this is how the existing buffer.set_span() method works.
// buf.get_mut(i, y).set_style(g.style);
buf[(x_hidden, y)].reset();
}
current_x = next_x;
x = next_x;
}
}
}
impl std::fmt::Display for Span<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.content)
/// A trait for converting a value to a [`Span`].
///
/// This trait is automatically implemented for any type that implements the [`Display`] trait. As
/// such, `ToSpan` shouln't be implemented directly: [`Display`] should be implemented instead, and
/// you get the `ToSpan` implementation for free.
///
/// [`Display`]: std::fmt::Display
pub trait ToSpan {
/// Converts the value to a [`Span`].
fn to_span(&self) -> Span<'_>;
}
/// # Panics
///
/// In this implementation, the `to_span` method panics if the `Display` implementation returns an
/// error. This indicates an incorrect `Display` implementation since `fmt::Write for String` never
/// returns an error itself.
impl<T: fmt::Display> ToSpan for T {
fn to_span(&self) -> Span<'_> {
Span::raw(self.to_string())
}
}
impl fmt::Display for Span<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for line in self.content.lines() {
fmt::Display::fmt(line, f)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use rstest::{fixture, rstest};
use super::*;
use crate::{buffer::Cell, layout::Alignment, style::Stylize};
#[fixture]
fn small_buf() -> Buffer {
Buffer::empty(Rect::new(0, 0, 10, 1))
}
#[test]
fn default() {
@@ -467,6 +599,12 @@ mod tests {
assert_eq!(span.style, Style::default());
}
#[test]
fn to_span() {
assert_eq!(42.to_span(), Span::raw("42"));
assert_eq!("test".to_span(), Span::raw("test"));
}
#[test]
fn reset_style() {
let span = Span::styled("test content", Style::new().green()).reset_style();
@@ -485,6 +623,8 @@ 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/ratatui/issues/1271
assert_eq!(Span::raw("test\ncontent").width(), 12);
}
#[test]
@@ -498,23 +638,52 @@ mod tests {
assert_eq!(stylized.content, Cow::Borrowed("test content"));
assert_eq!(stylized.style, Style::new().green().on_yellow().bold());
}
#[test]
fn display_span() {
let span = Span::raw("test content");
assert_eq!(format!("{span}"), "test content");
assert_eq!(format!("{span:.4}"), "test");
}
#[test]
fn display_newline_span() {
let span = Span::raw("test\ncontent");
assert_eq!(format!("{span}"), "testcontent");
}
#[test]
fn display_styled_span() {
let stylized_span = Span::styled("stylized test content", Style::new().green());
assert_eq!(format!("{stylized_span}"), "stylized test content");
assert_eq!(format!("{stylized_span:.8}"), "stylized");
}
#[test]
fn left_aligned() {
let span = Span::styled("Test Content", Style::new().green().italic());
let line = span.into_left_aligned_line();
assert_eq!(line.alignment, Some(Alignment::Left));
}
#[test]
fn centered() {
let span = Span::styled("Test Content", Style::new().green().italic());
let line = span.into_centered_line();
assert_eq!(line.alignment, Some(Alignment::Center));
}
#[test]
fn right_aligned() {
let span = Span::styled("Test Content", Style::new().green().italic());
let line = span.into_right_aligned_line();
assert_eq!(line.alignment, Some(Alignment::Right));
}
mod widget {
use rstest::rstest;
use super::*;
use crate::{assert_buffer_eq, style::Stylize};
#[test]
fn render() {
@@ -522,12 +691,21 @@ mod tests {
let span = Span::styled("test content", style);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
span.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![Line::from(vec![
let expected = Buffer::with_lines([Line::from(vec![
"test content".green().on_yellow(),
" ".into(),
])]);
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
}
#[rstest]
#[case::x(20, 0)]
#[case::y(0, 20)]
#[case::both(20, 20)]
fn render_out_of_bounds(mut small_buf: Buffer, #[case] x: u16, #[case] y: u16) {
let out_of_bounds = Rect::new(x, y, 10, 1);
Span::raw("Hello, World!").render(out_of_bounds, &mut small_buf);
assert_eq!(small_buf, Buffer::empty(small_buf.area));
}
/// When the content of the span is longer than the area passed to render, the content
@@ -540,10 +718,11 @@ mod tests {
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
span.render(Rect::new(0, 0, 5, 1), &mut buf);
let mut expected = Buffer::with_lines(vec![Line::from("test ")]);
expected.set_style(Rect::new(0, 0, 5, 1), (Color::Green, Color::Yellow));
assert_buffer_eq!(buf, expected);
let expected = Buffer::with_lines([Line::from(vec![
"test ".green().on_yellow(),
" ".into(),
])]);
assert_eq!(buf, expected);
}
/// When there is already a style set on the buffer, the style of the span should be
@@ -555,12 +734,11 @@ mod tests {
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
buf.set_style(buf.area, Style::new().italic());
span.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![Line::from(vec![
let expected = Buffer::with_lines([Line::from(vec![
"test content".green().on_yellow().italic(),
" ".italic(),
])]);
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
}
/// When the span contains a multi-width grapheme, the grapheme will ensure that the cells
@@ -571,12 +749,11 @@ mod tests {
let span = Span::styled("test 😃 content", style);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
span.render(buf.area, &mut buf);
// The existing code in buffer.set_line() handles multi-width graphemes by clearing the
// cells of the hidden characters. This test ensures that the existing behavior is
// preserved.
let expected = Buffer::with_lines(vec!["test 😃 content".green().on_yellow()]);
assert_buffer_eq!(buf, expected);
let expected = Buffer::with_lines(["test 😃 content".green().on_yellow()]);
assert_eq!(buf, expected);
}
/// When the span contains a multi-width grapheme that does not fit in the area passed to
@@ -589,11 +766,9 @@ mod tests {
let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
span.render(buf.area, &mut buf);
let expected = Buffer::with_lines(vec![Line::from(vec![
"test ".green().on_yellow(),
" ".into(),
])]);
assert_buffer_eq!(buf, expected);
let expected =
Buffer::with_lines([Line::from(vec!["test ".green().on_yellow(), " ".into()])]);
assert_eq!(buf, expected);
}
/// When the area passed to render overflows the buffer, the content should be truncated
@@ -605,32 +780,121 @@ mod tests {
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 1));
span.render(Rect::new(10, 0, 20, 1), &mut buf);
let expected = Buffer::with_lines(vec![Line::from(vec![
let expected = Buffer::with_lines([Line::from(vec![
" ".into(),
"test ".green().on_yellow(),
])]);
assert_buffer_eq!(buf, expected);
assert_eq!(buf, expected);
}
#[test]
fn render_first_zero_width() {
let span = Span::raw("\u{200B}abc");
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[Cell::new("\u{200B}a"), Cell::new("b"), Cell::new("c"),]
);
}
#[test]
fn render_second_zero_width() {
let span = Span::raw("a\u{200B}bc");
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[Cell::new("a\u{200B}"), Cell::new("b"), Cell::new("c")]
);
}
#[test]
fn render_middle_zero_width() {
let span = Span::raw("ab\u{200B}c");
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[Cell::new("a"), Cell::new("b\u{200B}"), Cell::new("c")]
);
}
#[test]
fn render_last_zero_width() {
let span = Span::raw("abc\u{200B}");
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[Cell::new("a"), Cell::new("b"), Cell::new("c\u{200B}")]
);
}
#[test]
fn render_with_newlines() {
let span = Span::raw("a\nb");
let mut buf = Buffer::empty(Rect::new(0, 0, 2, 1));
span.render(buf.area, &mut buf);
assert_eq!(buf.content(), [Cell::new("a"), Cell::new("b")]);
}
}
/// 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
/// to be exceeded (due to a position + 1 calculation that fails to account for the possibility
/// that the next position might not be available).
#[test]
fn left_aligned() {
let span = Span::styled("Test Content", Style::new().green().italic());
let line = span.to_left_aligned_line();
assert_eq!(line.alignment, Some(Alignment::Left));
fn issue_1160() {
let span = Span::raw("Hello\u{200E}");
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[
Cell::new("H"),
Cell::new("e"),
Cell::new("l"),
Cell::new("l"),
Cell::new("o\u{200E}"),
]
);
}
#[test]
fn centered() {
let span = Span::styled("Test Content", Style::new().green().italic());
let line = span.to_centered_line();
assert_eq!(line.alignment, Some(Alignment::Center));
fn add() {
assert_eq!(
Span::default() + Span::default(),
Line::from(vec![Span::default(), Span::default()])
);
assert_eq!(
Span::default() + Span::raw("test"),
Line::from(vec![Span::default(), Span::raw("test")])
);
assert_eq!(
Span::raw("test") + Span::default(),
Line::from(vec![Span::raw("test"), Span::default()])
);
assert_eq!(
Span::raw("test") + Span::raw("content"),
Line::from(vec![Span::raw("test"), Span::raw("content")])
);
}
#[test]
fn right_aligned() {
let span = Span::styled("Test Content", Style::new().green().italic());
let line = span.to_right_aligned_line();
assert_eq!(line.alignment, Some(Alignment::Right));
#[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);
}
}

882
src/text/text.rs → ratatui-core/src/text/text.rs Normal file → Executable file

File diff suppressed because it is too large Load Diff

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,22 +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 termion::{color as tcolor, style as tstyle};
use crate::{
use ratatui_core::{
backend::{Backend, ClearType, WindowSize},
buffer::Cell,
prelude::Rect,
layout::{Position, Size},
style::{Color, Modifier, Style},
};
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 +50,11 @@ use crate::{
/// ```rust,no_run
/// use std::io::{stderr, stdout};
///
/// use ratatui::prelude::*;
/// use termion::{raw::IntoRawMode, screen::IntoAlternateScreen};
/// use ratatui::{
/// backend::TermionBackend,
/// termion::{raw::IntoRawMode, screen::IntoAlternateScreen},
/// Terminal,
/// };
///
/// let writer = stdout().into_raw_mode()?.into_alternate_screen()?;
/// let mut backend = TermionBackend::new(writer);
@@ -59,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>
@@ -75,15 +88,42 @@ 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 fn new(writer: W) -> TermionBackend<W> {
TermionBackend { writer }
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
}
}
@@ -136,11 +176,13 @@ where
self.writer.flush()
}
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
termion::cursor::DetectCursorPos::cursor_pos(&mut self.writer).map(|(x, y)| (x - 1, y - 1))
fn get_cursor_position(&mut self) -> io::Result<Position> {
termion::cursor::DetectCursorPos::cursor_pos(&mut self.writer)
.map(|(x, y)| Position { x: x - 1, y: y - 1 })
}
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
let Position { x, y } = position.into();
write!(self.writer, "{}", termion::cursor::Goto(x + 1, y + 1))?;
self.writer.flush()
}
@@ -155,13 +197,13 @@ where
let mut fg = Color::Reset;
let mut bg = Color::Reset;
let mut modifier = Modifier::empty();
let mut last_pos: Option<(u16, u16)> = None;
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.0 + 1 && y == p.1) {
if !matches!(last_pos, Some(p) if x == p.x + 1 && y == p.y) {
write!(string, "{}", termion::cursor::Goto(x + 1, y + 1)).unwrap();
}
last_pos = Some((x, y));
last_pos = Some(Position { x, y });
if cell.modifier != modifier {
write!(
string,
@@ -193,12 +235,12 @@ where
)
}
fn size(&self) -> io::Result<Rect> {
fn size(&self) -> io::Result<Size> {
let terminal = termion::terminal_size()?;
Ok(Rect::new(0, 0, terminal.0, terminal.1))
Ok(Size::new(terminal.0, terminal.1))
}
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
fn window_size(&mut self) -> io::Result<WindowSize> {
Ok(WindowSize {
columns_rows: termion::terminal_size()?.into(),
pixels: termion::terminal_size_pixels()?.into(),
@@ -208,17 +250,38 @@ 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()
}
}
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
struct Fg(Color);
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
struct Bg(Color);
/// 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.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
struct ModifierDiff {
from: Modifier,
to: Modifier,
@@ -226,7 +289,6 @@ struct ModifierDiff {
impl fmt::Display for Fg {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use termion::color::Color as TermionColor;
match self.0 {
Color::Reset => termion::color::Reset.write_fg(f),
Color::Black => termion::color::Black.write_fg(f),
@@ -252,7 +314,6 @@ impl fmt::Display for Fg {
}
impl fmt::Display for Bg {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use termion::color::Color as TermionColor;
match self.0 {
Color::Reset => termion::color::Reset.write_bg(f),
Color::Black => termion::color::Black.write_bg(f),
@@ -277,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 {
($termion_color:ident, $color:ident) => {
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)
}
}
@@ -317,39 +396,39 @@ from_termion_for_color!(LightMagenta, LightMagenta);
from_termion_for_color!(LightCyan, LightCyan);
from_termion_for_color!(LightWhite, White);
impl From<tcolor::AnsiValue> for Color {
fn from(value: tcolor::AnsiValue) -> Self {
Color::Indexed(value.0)
impl 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 {
Style::default().bg(Color::Indexed(value.0 .0))
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 {
Style::default().fg(Color::Indexed(value.0 .0))
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 {
Color::Rgb(value.0, value.1, value.2)
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 {
Style::default().bg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
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 {
Style::default().fg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
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))
}
}
@@ -419,9 +498,9 @@ 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 {
($termion_modifier:ident, $modifier:ident) => {
impl FromTermion<tstyle::$termion_modifier> for Modifier {
fn from_termion(_: tstyle::$termion_modifier) -> Self {
Modifier::$modifier
}
}
@@ -436,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 {
Modifier::empty()
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]
@@ -475,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))
);
}
@@ -516,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 -->

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