Compare commits

...

32 Commits
latest ... main

Author SHA1 Message Date
Josh McKinney
720303e806 fix: Align clear() semantics with contract (#2320) 2026-01-13 04:46:06 -08:00
dependabot[bot]
5f0331ec89 build(deps): bump taiki-e/install-action from 2.65.13 to 2.66.1 (#2344)
Bumps
[taiki-e/install-action](https://github.com/taiki-e/install-action) from
2.65.13 to 2.66.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/taiki-e/install-action/releases">taiki-e/install-action's
releases</a>.</em></p>
<blockquote>
<h2>2.66.1</h2>
<ul>
<li>
<p>Update <code>tombi@latest</code> to 0.7.18.</p>
</li>
<li>
<p>Update <code>ubi@latest</code> to 0.9.0.</p>
</li>
</ul>
<h2>2.66.0</h2>
<ul>
<li>
<p>Support <code>mdbook-mermaid-ssr</code>. (<a
href="https://redirect.github.com/taiki-e/install-action/pull/1400">#1400</a>,
thanks <a
href="https://github.com/CommanderStorm"><code>@​CommanderStorm</code></a>)</p>
</li>
<li>
<p>Improve support for Windows with MSYS2 bash.</p>
</li>
<li>
<p>Documentation improvements.</p>
</li>
</ul>
<h2>2.65.16</h2>
<ul>
<li>
<p>Update <code>zola@latest</code> to 0.22.0.</p>
</li>
<li>
<p>Update <code>wasmtime@latest</code> to 40.0.1.</p>
</li>
<li>
<p>Update <code>vacuum@latest</code> to 0.23.2.</p>
</li>
<li>
<p>Update <code>uv@latest</code> to 0.9.24.</p>
</li>
<li>
<p>Update <code>typos@latest</code> to 1.42.0.</p>
</li>
<li>
<p>Update <code>tombi@latest</code> to 0.7.16.</p>
</li>
<li>
<p>Update <code>syft@latest</code> to 1.40.0.</p>
</li>
<li>
<p>Update <code>protoc@latest</code> to 3.33.3.</p>
</li>
<li>
<p>Update <code>prek@latest</code> to 0.2.27.</p>
</li>
<li>
<p>Update <code>mise@latest</code> to 2026.1.1.</p>
</li>
<li>
<p>Update <code>cargo-nextest@latest</code> to 0.9.120.</p>
</li>
<li>
<p>Update <code>cargo-deny@latest</code> to 0.19.0.</p>
</li>
</ul>
<h2>2.65.15</h2>
<ul>
<li>
<p>Update <code>parse-dockerfile@latest</code> to 0.1.3.</p>
</li>
<li>
<p>Update <code>parse-changelog@latest</code> to 0.6.15.</p>
</li>
<li>
<p>Update <code>cargo-llvm-cov@latest</code> to 0.6.23.</p>
</li>
<li>
<p>Update <code>cargo-hack@latest</code> to 0.6.41.</p>
</li>
<li>
<p>Update <code>cargo-minimal-versions@latest</code> to 0.1.35.</p>
</li>
<li>
<p>Update <code>cargo-no-dev-deps@latest</code> to 0.2.20.</p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md">taiki-e/install-action's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<p>All notable changes to this project will be documented in this
file.</p>
<p>This project adheres to <a href="https://semver.org">Semantic
Versioning</a>.</p>
<!-- raw HTML omitted -->
<h2>[Unreleased]</h2>
<ul>
<li>
<p>Update <code>protoc@latest</code> to 3.33.4.</p>
</li>
<li>
<p>Update <code>knope@latest</code> to 0.22.0.</p>
</li>
</ul>
<h2>[2.66.1] - 2026-01-11</h2>
<ul>
<li>
<p>Update <code>tombi@latest</code> to 0.7.18.</p>
</li>
<li>
<p>Update <code>ubi@latest</code> to 0.9.0.</p>
</li>
</ul>
<h2>[2.66.0] - 2026-01-10</h2>
<ul>
<li>
<p>Support <code>mdbook-mermaid-ssr</code>. (<a
href="https://redirect.github.com/taiki-e/install-action/pull/1400">#1400</a>,
thanks <a
href="https://github.com/CommanderStorm"><code>@​CommanderStorm</code></a>)</p>
</li>
<li>
<p>Improve support for Windows with MSYS2 bash.</p>
</li>
<li>
<p>Documentation improvements.</p>
</li>
</ul>
<h2>[2.65.16] - 2026-01-10</h2>
<ul>
<li>
<p>Update <code>zola@latest</code> to 0.22.0.</p>
</li>
<li>
<p>Update <code>wasmtime@latest</code> to 40.0.1.</p>
</li>
<li>
<p>Update <code>vacuum@latest</code> to 0.23.2.</p>
</li>
<li>
<p>Update <code>uv@latest</code> to 0.9.24.</p>
</li>
<li>
<p>Update <code>typos@latest</code> to 1.42.0.</p>
</li>
<li>
<p>Update <code>tombi@latest</code> to 0.7.16.</p>
</li>
<li>
<p>Update <code>syft@latest</code> to 1.40.0.</p>
</li>
<li>
<p>Update <code>protoc@latest</code> to 3.33.3.</p>
</li>
<li>
<p>Update <code>prek@latest</code> to 0.2.27.</p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="3522286d40"><code>3522286</code></a>
Release 2.66.1</li>
<li><a
href="0ed1c967ed"><code>0ed1c96</code></a>
Update changelog</li>
<li><a
href="e69513dca4"><code>e69513d</code></a>
Update <code>tombi@latest</code> to 0.7.18</li>
<li><a
href="4c5e3dc538"><code>4c5e3dc</code></a>
Update <code>ubi@latest</code> to 0.9.0</li>
<li><a
href="e2cc276530"><code>e2cc276</code></a>
Update <code>tombi@latest</code> to 0.7.17</li>
<li><a
href="8fb4e4b842"><code>8fb4e4b</code></a>
Update .deny.toml</li>
<li><a
href="83961fc0a2"><code>83961fc</code></a>
Release 2.66.0</li>
<li><a
href="c761556c39"><code>c761556</code></a>
Update changelog</li>
<li><a
href="f19ed92a93"><code>f19ed92</code></a>
Workaround MSYS2 bash issue</li>
<li><a
href="e53d9e37e3"><code>e53d9e3</code></a>
docs: Clarify &quot;tool is supported&quot; doesn't mean &quot;tool is
trusted or reviewed b...</li>
<li>Additional commits viewable in <a
href="0e76c5c569...3522286d40">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=taiki-e/install-action&package-manager=github_actions&previous-version=2.65.13&new-version=2.66.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>
2026-01-13 10:27:34 +03:00
dependabot[bot]
eeed03cba2 build(deps): bump EmbarkStudios/cargo-deny-action from 2.0.14 to 2.0.15 (#2345)
Bumps
[EmbarkStudios/cargo-deny-action](https://github.com/embarkstudios/cargo-deny-action)
from 2.0.14 to 2.0.15.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="3fd3802e88"><code>3fd3802</code></a>
Bump to 0.19.0</li>
<li>See full diff in <a
href="76cd80eb77...3fd3802e88">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=2.0.14&new-version=2.0.15)](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>
2026-01-13 10:27:20 +03:00
dependabot[bot]
c3b575e24e build(deps): bump crate-ci/typos from 1.41.0 to 1.42.0 (#2346)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.41.0 to
1.42.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/crate-ci/typos/releases">crate-ci/typos's
releases</a>.</em></p>
<blockquote>
<h2>v1.42.0</h2>
<h2>[1.42.0] - 2026-01-07</h2>
<h3>Features</h3>
<ul>
<li>Dictionary updates</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/crate-ci/typos/blob/master/CHANGELOG.md">crate-ci/typos's
changelog</a>.</em></p>
<blockquote>
<h1>Change Log</h1>
<p>All notable changes to this project will be documented in this
file.</p>
<p>The format is based on <a href="https://keepachangelog.com/">Keep a
Changelog</a>
and this project adheres to <a href="https://semver.org/">Semantic
Versioning</a>.</p>
<!-- raw HTML omitted -->
<h2>[Unreleased] - ReleaseDate</h2>
<h2>[1.42.0] - 2026-01-07</h2>
<h3>Features</h3>
<ul>
<li>Dictionary updates</li>
</ul>
<h2>[1.41.0] - 2025-12-31</h2>
<h3>Features</h3>
<ul>
<li>Updated the dictionary with the <a
href="https://redirect.github.com/crate-ci/typos/issues/1431">December
2025</a> changes</li>
</ul>
<h2>[1.40.1] - 2025-12-29</h2>
<h3>Fixes</h3>
<ul>
<li>Treat <code>incrementer</code> and <code>incrementor</code> the same
for now</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Don't correct ITerm2</li>
</ul>
<h2>[1.40.0] - 2025-11-26</h2>
<h3>Features</h3>
<ul>
<li>Updated the dictionary with the <a
href="https://redirect.github.com/crate-ci/typos/issues/1405">November
2025</a> changes</li>
</ul>
<h2>[1.39.2] - 2025-11-13</h2>
<h3>Fixes</h3>
<ul>
<li>Don't offer <code>entry</code> as a correction for
<code>entrys</code></li>
</ul>
<h2>[1.39.1] - 2025-11-12</h2>
<h3>Features</h3>
<ul>
<li>Make <code>--help</code> more vibrant</li>
</ul>
<h2>[1.39.0] - 2025-10-31</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="bb4666ad77"><code>bb4666a</code></a>
chore: Release</li>
<li><a
href="6995a8a1d2"><code>6995a8a</code></a>
chore: Release</li>
<li><a
href="e0227ba473"><code>e0227ba</code></a>
docs: Update changelog</li>
<li><a
href="c4054dbccf"><code>c4054db</code></a>
Merge pull request <a
href="https://redirect.github.com/crate-ci/typos/issues/1460">#1460</a>
from epage/wiki</li>
<li><a
href="596a0bd11d"><code>596a0bd</code></a>
feat(dict): Extend from misspell's list</li>
<li><a
href="2d459793db"><code>2d45979</code></a>
refactor(misspell): Reformat dict to look like ours</li>
<li><a
href="54e1366637"><code>54e1366</code></a>
feat(dict): Extend from codespell's list</li>
<li><a
href="74e0660315"><code>74e0660</code></a>
feat(dict): Extend from wikipedia's list</li>
<li><a
href="a2568ec213"><code>a2568ec</code></a>
Merge pull request <a
href="https://redirect.github.com/crate-ci/typos/issues/1459">#1459</a>
from epage/update</li>
<li><a
href="c360cf6c3d"><code>c360cf6</code></a>
fix(wikipedia): Update dict</li>
<li>Additional commits viewable in <a
href="5c19779cb5...bb4666ad77">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=crate-ci/typos&package-manager=github_actions&previous-version=1.41.0&new-version=1.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>
2026-01-13 10:27:05 +03:00
dependabot[bot]
6fdb3fa18e build(deps): bump serde_json from 1.0.148 to 1.0.149 (#2347)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.148 to
1.0.149.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/serde-rs/json/releases">serde_json's
releases</a>.</em></p>
<blockquote>
<h2>v1.0.149</h2>
<ul>
<li>Align arbitrary_precision number strings with zmij's formatting (<a
href="https://redirect.github.com/serde-rs/json/issues/1306">#1306</a>,
thanks <a href="https://github.com/b41sh"><code>@​b41sh</code></a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="4f6dbfac79"><code>4f6dbfa</code></a>
Release 1.0.149</li>
<li><a
href="f3df680098"><code>f3df680</code></a>
Touch up PR 1306</li>
<li><a
href="e16730ff44"><code>e16730f</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/json/issues/1306">#1306</a>
from b41sh/fix-float-number-display</li>
<li><a
href="eeb2bcd3f2"><code>eeb2bcd</code></a>
Align <code>arbitrary_precision</code> number strings with zmij’s
formatting</li>
<li>See full diff in <a
href="https://github.com/serde-rs/json/compare/v1.0.148...v1.0.149">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=serde_json&package-manager=cargo&previous-version=1.0.148&new-version=1.0.149)](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>
2026-01-13 10:26:49 +03:00
Dheepak Krishnamurthy
ab5ad3fc7c chore: escape username using backticks in changelog (#2336)
The motivation behind this is that GitHub mentions in PR descriptions trigger notifications for every contributor for every release because their username is part of the changelog.
2026-01-07 18:02:37 -08:00
dependabot[bot]
332658406c build(deps): bump lru from 0.16.2 to 0.16.3 in the cargo group across 1 directory (#2337)
Bumps the cargo group with 1 update in the / directory:
[lru](https://github.com/jeromefroe/lru-rs).

Updates `lru` from 0.16.2 to 0.16.3
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/jeromefroe/lru-rs/blob/master/CHANGELOG.md">lru's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/jeromefroe/lru-rs/tree/0.16.3">v0.16.3</a> -
2026-01-07</h2>
<ul>
<li>Fix Stacked Borrows violation in <code>IterMut</code>.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="af233e5c36"><code>af233e5</code></a>
Merge pull request <a
href="https://redirect.github.com/jeromefroe/lru-rs/issues/225">#225</a>
from jeromefroe/jerome/prepare-0-16-3-release</li>
<li><a
href="cf56f9a5dd"><code>cf56f9a</code></a>
Prepare 0.16.3 release</li>
<li><a
href="62be24c961"><code>62be24c</code></a>
Merge pull request <a
href="https://redirect.github.com/jeromefroe/lru-rs/issues/224">#224</a>
from paolobarbolini/iter-mut-stacked-borrows-violation</li>
<li><a
href="25669e7611"><code>25669e7</code></a>
Add regression test for <code>IterMut</code> stacked borrows
violation</li>
<li><a
href="b9bca3492d"><code>b9bca34</code></a>
Fix stacked borrows violation in <code>IterMut::next</code> and
<code>IterMut::next_back</code></li>
<li>See full diff in <a
href="https://github.com/jeromefroe/lru-rs/compare/0.16.2...0.16.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=lru&package-manager=cargo&previous-version=0.16.2&new-version=0.16.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 <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/ratatui/ratatui/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-07 18:00:57 -08:00
Josh McKinney
ae975c7200 feat(examples): allow overlap spacing in explorer (#2316)
Store spacing as i16 so negative values map to Spacing::Overlap, and
show overlap in the axis label.

Co-authored-by: Orhun Parmaksız <orhun@archlinux.org>
2026-01-06 17:23:44 -08:00
Josh McKinney
3df8c20dfb docs: ask issue authors about PR willingness (#2317)
Add a contribution question to the bug report and feature request
templates so maintainers know whether the reporter wants to work on a
fix/PR or needs guidance.
2026-01-06 17:22:48 -08:00
dependabot[bot]
2dcf1a61c1 build(deps): bump rsa from 0.9.9 to 0.9.10 in the cargo group across 1 directory (#2334)
Bumps the cargo group with 1 update in the / directory:
[rsa](https://github.com/RustCrypto/RSA).

Updates `rsa` from 0.9.9 to 0.9.10
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/RustCrypto/RSA/blob/v0.9.10/CHANGELOG.md">rsa's
changelog</a>.</em></p>
<blockquote>
<h2>0.9.10 (2026-01-06)</h2>
<h3>Fixed</h3>
<ul>
<li>do not panic on a prime being 1 when loading a secret key (<a
href="https://redirect.github.com/RustCrypto/RSA/issues/624">#624</a>)</li>
</ul>
<p><a
href="https://redirect.github.com/RustCrypto/RSA/issues/624">#624</a>:
<a
href="https://redirect.github.com/RustCrypto/RSA/pull/624">RustCrypto/RSA#624</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="da2af9a0ff"><code>da2af9a</code></a>
chore: release v0.9.10</li>
<li><a
href="2926c91bef"><code>2926c91</code></a>
fix: do not panic on a prime being 1 when loading a secret key (<a
href="https://redirect.github.com/RustCrypto/RSA/issues/624">#624</a>)</li>
<li>See full diff in <a
href="https://github.com/RustCrypto/RSA/compare/v0.9.9...v0.9.10">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=rsa&package-manager=cargo&previous-version=0.9.9&new-version=0.9.10)](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 <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/ratatui/ratatui/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-07 00:31:02 +03:00
dependabot[bot]
6ce4a7e84b build(deps): bump octocrab from 0.49.4 to 0.49.5 (#2328)
Bumps [octocrab](https://github.com/XAMPPRocky/octocrab) from 0.49.4 to
0.49.5.
<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.49.5</h2>
<h3>Fixed</h3>
<ul>
<li>resolve docs.rs build failure (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/848">#848</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.49.4...v0.49.5">0.49.5</a>
- 2025-12-30</h2>
<h3>Fixed</h3>
<ul>
<li>resolve docs.rs build failure (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/848">#848</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="d4fde40841"><code>d4fde40</code></a>
chore: release v0.49.5 (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/849">#849</a>)</li>
<li><a
href="81da9b10e9"><code>81da9b1</code></a>
fix: resolve docs.rs build failure (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/848">#848</a>)</li>
<li>See full diff in <a
href="https://github.com/XAMPPRocky/octocrab/compare/v0.49.4...v0.49.5">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.49.4&new-version=0.49.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>
2026-01-05 23:13:49 +03:00
dependabot[bot]
e0687aa155 build(deps): bump tokio from 1.48.0 to 1.49.0 (#2327)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.48.0 to 1.49.0.
<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.49.0</h2>
<h1>1.49.0 (January 3rd, 2026)</h1>
<h3>Added</h3>
<ul>
<li>net: add support for <code>TCLASS</code> option on IPv6 (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7781">#7781</a>)</li>
<li>runtime: stabilize <code>runtime::id::Id</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7125">#7125</a>)</li>
<li>task: implement <code>Extend</code> for <code>JoinSet</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7195">#7195</a>)</li>
<li>task: stabilize the <code>LocalSet::id()</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7776">#7776</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>net: deprecate <code>{TcpStream,TcpSocket}::set_linger</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7752">#7752</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>macros: fix the hygiene issue of <code>join!</code> and
<code>try_join!</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7766">#7766</a>)</li>
<li>runtime: revert &quot;replace manual vtable definitions with
Wake&quot; (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7699">#7699</a>)</li>
<li>sync: return <code>TryRecvError::Disconnected</code> from
<code>Receiver::try_recv</code> after <code>Receiver::close</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7686">#7686</a>)</li>
<li>task: remove unnecessary trait bounds on the <code>Debug</code>
implementation (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7720">#7720</a>)</li>
</ul>
<h3>Unstable</h3>
<ul>
<li>fs: handle <code>EINTR</code> in <code>fs::write</code> for io-uring
(<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7786">#7786</a>)</li>
<li>fs: support io-uring with <code>tokio::fs::read</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7696">#7696</a>)</li>
<li>runtime: disable io-uring on <code>EPERM</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7724">#7724</a>)</li>
<li>time: add alternative timer for better multicore scalability (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7467">#7467</a>)</li>
</ul>
<h3>Documented</h3>
<ul>
<li>docs: fix a typos in <code>bounded.rs</code> and
<code>park.rs</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7817">#7817</a>)</li>
<li>io: add <code>SyncIoBridge</code> cross-references to
<code>copy</code> and <code>copy_buf</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7798">#7798</a>)</li>
<li>io: doc that <code>AsyncWrite</code> does not inherit from
<code>std::io::Write</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7705">#7705</a>)</li>
<li>metrics: clarify that <code>num_alive_tasks</code> is not strongly
consistent (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7614">#7614</a>)</li>
<li>net: clarify the cancellation safety of the
<code>TcpStream::peek</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7305">#7305</a>)</li>
<li>net: clarify the drop behavior of <code>unix::OwnedWriteHalf</code>
(<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7742">#7742</a>)</li>
<li>net: clarify the platform-dependent backlog in
<code>TcpSocket</code> docs (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7738">#7738</a>)</li>
<li>runtime: mention <code>LocalRuntime</code> in
<code>new_current_thread</code> docs (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7820">#7820</a>)</li>
<li>sync: add missing period to <code>mpsc::Sender::try_send</code> docs
(<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7721">#7721</a>)</li>
<li>sync: clarify the cancellation safety of
<code>oneshot::Receiver</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7780">#7780</a>)</li>
<li>sync: improve the docs for the <code>errors</code> of mpsc (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7722">#7722</a>)</li>
<li>task: add example for <code>spawn_local</code> usage on local
runtime (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7689">#7689</a>)</li>
</ul>
<p><a
href="https://redirect.github.com/tokio-rs/tokio/issues/7125">#7125</a>:
<a
href="https://redirect.github.com/tokio-rs/tokio/pull/7125">tokio-rs/tokio#7125</a>
<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7195">#7195</a>:
<a
href="https://redirect.github.com/tokio-rs/tokio/pull/7195">tokio-rs/tokio#7195</a>
<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7305">#7305</a>:
<a
href="https://redirect.github.com/tokio-rs/tokio/pull/7305">tokio-rs/tokio#7305</a>
<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7467">#7467</a>:
<a
href="https://redirect.github.com/tokio-rs/tokio/pull/7467">tokio-rs/tokio#7467</a>
<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7614">#7614</a>:
<a
href="https://redirect.github.com/tokio-rs/tokio/pull/7614">tokio-rs/tokio#7614</a>
<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7686">#7686</a>:
<a
href="https://redirect.github.com/tokio-rs/tokio/pull/7686">tokio-rs/tokio#7686</a>
<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7689">#7689</a>:
<a
href="https://redirect.github.com/tokio-rs/tokio/pull/7689">tokio-rs/tokio#7689</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="e3b89bbefa"><code>e3b89bb</code></a>
chore: prepare Tokio v1.49.0 (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7824">#7824</a>)</li>
<li><a
href="4f577b84e9"><code>4f577b8</code></a>
Merge 'tokio-1.47.3' into 'master'</li>
<li><a
href="f320197693"><code>f320197</code></a>
chore: prepare Tokio v1.47.3 (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7823">#7823</a>)</li>
<li><a
href="ea6b144cd1"><code>ea6b144</code></a>
ci: freeze rustc on nightly-2025-01-25 in <code>netlify.toml</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7652">#7652</a>)</li>
<li><a
href="264e703296"><code>264e703</code></a>
Merge <code>tokio-1.43.4</code> into <code>tokio-1.47.x</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7822">#7822</a>)</li>
<li><a
href="dfb0f00838"><code>dfb0f00</code></a>
chore: prepare Tokio v1.43.4 (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7821">#7821</a>)</li>
<li><a
href="4a91f197b0"><code>4a91f19</code></a>
ci: fix wasm32-wasip1 tests (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7788">#7788</a>)</li>
<li><a
href="601c383ab6"><code>601c383</code></a>
ci: upgrade FreeBSD from 14.2 to 14.3 (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7758">#7758</a>)</li>
<li><a
href="484cb52d8d"><code>484cb52</code></a>
sync: return <code>TryRecvError::Disconnected</code> from
<code>Receiver::try_recv</code> after `Re...</li>
<li><a
href="16f20c34ed"><code>16f20c3</code></a>
rt: mention <code>LocalRuntime</code> in <code>new_current_thread</code>
docs (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7820">#7820</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/tokio-rs/tokio/compare/tokio-1.48.0...tokio-1.49.0">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.48.0&new-version=1.49.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>
2026-01-05 23:13:30 +03:00
dependabot[bot]
0346476ddf build(deps): bump crate-ci/typos from 1.40.0 to 1.41.0 (#2325)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.40.0 to
1.41.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/crate-ci/typos/releases">crate-ci/typos's
releases</a>.</em></p>
<blockquote>
<h2>v1.41.0</h2>
<h2>[1.41.0] - 2025-12-31</h2>
<h3>Features</h3>
<ul>
<li>Updated the dictionary with the <a
href="https://redirect.github.com/crate-ci/typos/issues/1431">December
2025</a> changes</li>
</ul>
<h2>v1.40.1</h2>
<h2>[1.40.1] - 2025-12-29</h2>
<h3>Fixes</h3>
<ul>
<li>Treat <code>incrementer</code> and <code>incrementor</code> the same
for now</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Don't correct ITerm2</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/crate-ci/typos/blob/master/CHANGELOG.md">crate-ci/typos's
changelog</a>.</em></p>
<blockquote>
<h1>Change Log</h1>
<p>All notable changes to this project will be documented in this
file.</p>
<p>The format is based on <a href="https://keepachangelog.com/">Keep a
Changelog</a>
and this project adheres to <a href="https://semver.org/">Semantic
Versioning</a>.</p>
<!-- raw HTML omitted -->
<h2>[Unreleased] - ReleaseDate</h2>
<h2>[1.41.0] - 2025-12-31</h2>
<h3>Features</h3>
<ul>
<li>Updated the dictionary with the <a
href="https://redirect.github.com/crate-ci/typos/issues/1431">December
2025</a> changes</li>
</ul>
<h2>[1.40.1] - 2025-12-29</h2>
<h3>Fixes</h3>
<ul>
<li>Treat <code>incrementer</code> and <code>incrementor</code> the same
for now</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Don't correct ITerm2</li>
</ul>
<h2>[1.40.0] - 2025-11-26</h2>
<h3>Features</h3>
<ul>
<li>Updated the dictionary with the <a
href="https://redirect.github.com/crate-ci/typos/issues/1405">November
2025</a> changes</li>
</ul>
<h2>[1.39.2] - 2025-11-13</h2>
<h3>Fixes</h3>
<ul>
<li>Don't offer <code>entry</code> as a correction for
<code>entrys</code></li>
</ul>
<h2>[1.39.1] - 2025-11-12</h2>
<h3>Features</h3>
<ul>
<li>Make <code>--help</code> more vibrant</li>
</ul>
<h2>[1.39.0] - 2025-10-31</h2>
<h3>Features</h3>
<ul>
<li>Updated the dictionary with the <a
href="https://redirect.github.com/crate-ci/typos/issues/1383">October
2025</a> changes</li>
</ul>
<h3>Fixes</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="5c19779cb5"><code>5c19779</code></a>
chore: Release</li>
<li><a
href="cf11fdd0ca"><code>cf11fdd</code></a>
chore: Release</li>
<li><a
href="54e83d2a58"><code>54e83d2</code></a>
docs: Update changelog</li>
<li><a
href="fbd7b69944"><code>fbd7b69</code></a>
Merge pull request <a
href="https://redirect.github.com/crate-ci/typos/issues/1454">#1454</a>
from epage/dec</li>
<li><a
href="5dc35c7a63"><code>5dc35c7</code></a>
feat(dict): December additions</li>
<li><a
href="1a319b54cc"><code>1a319b5</code></a>
chore: Release</li>
<li><a
href="00852bb03b"><code>00852bb</code></a>
docs: Update changelog</li>
<li><a
href="1d4327057a"><code>1d43270</code></a>
chore: Release</li>
<li><a
href="770146db44"><code>770146d</code></a>
Merge pull request <a
href="https://redirect.github.com/crate-ci/typos/issues/1452">#1452</a>
from epage/incrementer</li>
<li><a
href="6bf28995c6"><code>6bf2899</code></a>
fix(dict): Be neutral on incrementer vs incrementor</li>
<li>Additional commits viewable in <a
href="2d0ce569fe...5c19779cb5">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=crate-ci/typos&package-manager=github_actions&previous-version=1.40.0&new-version=1.41.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>
2026-01-05 23:13:10 +03:00
dependabot[bot]
fd8c1a4010 build(deps): bump taiki-e/install-action from 2.65.7 to 2.65.13 (#2326)
Bumps
[taiki-e/install-action](https://github.com/taiki-e/install-action) from
2.65.7 to 2.65.13.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/taiki-e/install-action/releases">taiki-e/install-action's
releases</a>.</em></p>
<blockquote>
<h2>2.65.13</h2>
<ul>
<li>
<p>Update <code>cargo-nextest@latest</code> to 0.9.118.</p>
</li>
<li>
<p>Update <code>martin@latest</code> to 1.2.0.</p>
</li>
<li>
<p>Update <code>cargo-insta@latest</code> to 1.46.0.</p>
</li>
</ul>
<h2>2.65.12</h2>
<ul>
<li>
<p>Update <code>just@latest</code> to 1.46.0.</p>
</li>
<li>
<p>Update <code>cargo-nextest@latest</code> to 0.9.117.</p>
</li>
</ul>
<h2>2.65.11</h2>
<ul>
<li>
<p>Update <code>cargo-tarpaulin@latest</code> to 0.35.0.</p>
</li>
<li>
<p>Update <code>typos@latest</code> to 1.41.0.</p>
</li>
<li>
<p>Update <code>vacuum@latest</code> to 0.23.0.</p>
</li>
</ul>
<h2>2.65.10</h2>
<ul>
<li>
<p>Update <code>mise@latest</code> to 2025.12.13.</p>
</li>
<li>
<p>Update <code>uv@latest</code> to 0.9.21.</p>
</li>
</ul>
<h2>2.65.9</h2>
<ul>
<li>Update <code>cargo-llvm-cov@latest</code> to 0.6.22.</li>
</ul>
<h2>2.65.8</h2>
<ul>
<li>
<p>Update <code>tombi@latest</code> to 0.7.14.</p>
</li>
<li>
<p>Update <code>uv@latest</code> to 0.9.20.</p>
</li>
<li>
<p>Update <code>typos@latest</code> to 1.40.1.</p>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md">taiki-e/install-action's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<p>All notable changes to this project will be documented in this
file.</p>
<p>This project adheres to <a href="https://semver.org">Semantic
Versioning</a>.</p>
<!-- raw HTML omitted -->
<h2>[Unreleased]</h2>
<h2>[2.65.13] - 2026-01-05</h2>
<ul>
<li>
<p>Update <code>cargo-nextest@latest</code> to 0.9.118.</p>
</li>
<li>
<p>Update <code>martin@latest</code> to 1.2.0.</p>
</li>
<li>
<p>Update <code>cargo-insta@latest</code> to 1.46.0.</p>
</li>
</ul>
<h2>[2.65.12] - 2026-01-02</h2>
<ul>
<li>
<p>Update <code>just@latest</code> to 1.46.0.</p>
</li>
<li>
<p>Update <code>cargo-nextest@latest</code> to 0.9.117.</p>
</li>
</ul>
<h2>[2.65.11] - 2026-01-01</h2>
<ul>
<li>
<p>Update <code>cargo-tarpaulin@latest</code> to 0.35.0.</p>
</li>
<li>
<p>Update <code>typos@latest</code> to 1.41.0.</p>
</li>
<li>
<p>Update <code>vacuum@latest</code> to 0.23.0.</p>
</li>
</ul>
<h2>[2.65.10] - 2025-12-31</h2>
<ul>
<li>
<p>Update <code>mise@latest</code> to 2025.12.13.</p>
</li>
<li>
<p>Update <code>uv@latest</code> to 0.9.21.</p>
</li>
</ul>
<h2>[2.65.9] - 2025-12-30</h2>
<ul>
<li>Update <code>cargo-llvm-cov@latest</code> to 0.6.22.</li>
</ul>
<h2>[2.65.8] - 2025-12-30</h2>
<ul>
<li>
<p>Update <code>tombi@latest</code> to 0.7.14.</p>
</li>
<li>
<p>Update <code>uv@latest</code> to 0.9.20.</p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="0e76c5c569"><code>0e76c5c</code></a>
Release 2.65.13</li>
<li><a
href="0466464eeb"><code>0466464</code></a>
Update <code>cargo-nextest@latest</code> to 0.9.118</li>
<li><a
href="389b56344a"><code>389b563</code></a>
Update <code>martin@latest</code> to 1.2.0</li>
<li><a
href="83028a3bd2"><code>83028a3</code></a>
Update <code>cargo-insta@latest</code> to 1.46.0</li>
<li><a
href="cc33365ec7"><code>cc33365</code></a>
Release 2.65.12</li>
<li><a
href="8f085a196a"><code>8f085a1</code></a>
Update <code>just@latest</code> to 1.46.0</li>
<li><a
href="6b0d292eb8"><code>6b0d292</code></a>
Update <code>cargo-nextest@latest</code> to 0.9.117</li>
<li><a
href="a983ca7951"><code>a983ca7</code></a>
Release 2.65.11</li>
<li><a
href="66de739d66"><code>66de739</code></a>
Update <code>cargo-tarpaulin@latest</code> to 0.35.0</li>
<li><a
href="790813cf48"><code>790813c</code></a>
Update <code>typos@latest</code> to 1.41.0</li>
<li>Additional commits viewable in <a
href="4c6723ec9c...0e76c5c569">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=taiki-e/install-action&package-manager=github_actions&previous-version=2.65.7&new-version=2.65.13)](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>
2026-01-05 23:12:56 +03:00
dependabot[bot]
dd6621daf9 build(deps): bump tokio-stream from 0.1.17 to 0.1.18 (#2330)
Bumps [tokio-stream](https://github.com/tokio-rs/tokio) from 0.1.17 to
0.1.18.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="60b083b630"><code>60b083b</code></a>
chore: prepare tokio-stream 0.1.18 (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7830">#7830</a>)</li>
<li><a
href="9cc02cc88d"><code>9cc02cc</code></a>
chore: prepare tokio-util 0.7.18 (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7829">#7829</a>)</li>
<li><a
href="d2799d791b"><code>d2799d7</code></a>
task: improve the docs of <code>Builder::spawn_local</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7828">#7828</a>)</li>
<li><a
href="4d4870f291"><code>4d4870f</code></a>
task: doc that task drops before JoinHandle completion (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7825">#7825</a>)</li>
<li><a
href="fdb150901a"><code>fdb1509</code></a>
fs: check for io-uring opcode support (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7815">#7815</a>)</li>
<li><a
href="426a562780"><code>426a562</code></a>
rt: remove <code>allow(dead_code)</code> after <code>JoinSet</code>
stabilization (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7826">#7826</a>)</li>
<li><a
href="e3b89bbefa"><code>e3b89bb</code></a>
chore: prepare Tokio v1.49.0 (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7824">#7824</a>)</li>
<li><a
href="4f577b84e9"><code>4f577b8</code></a>
Merge 'tokio-1.47.3' into 'master'</li>
<li><a
href="f320197693"><code>f320197</code></a>
chore: prepare Tokio v1.47.3 (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7823">#7823</a>)</li>
<li><a
href="ea6b144cd1"><code>ea6b144</code></a>
ci: freeze rustc on nightly-2025-01-25 in <code>netlify.toml</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7652">#7652</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/tokio-rs/tokio/compare/tokio-stream-0.1.17...tokio-stream-0.1.18">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tokio-stream&package-manager=cargo&previous-version=0.1.17&new-version=0.1.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>
2026-01-05 23:11:56 +03:00
dependabot[bot]
d37db578b8 build(deps): bump clap from 4.5.53 to 4.5.54 (#2331)
Bumps [clap](https://github.com/clap-rs/clap) from 4.5.53 to 4.5.54.
<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.54</h2>
<h2>[4.5.54] - 2026-01-02</h2>
<h3>Fixes</h3>
<ul>
<li><em>(help)</em> Move <code>[default]</code> to its own paragraph
when <code>PossibleValue::help</code> is present in
<code>--help</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.54] - 2026-01-02</h2>
<h3>Fixes</h3>
<ul>
<li><em>(help)</em> Move <code>[default]</code> to its own paragraph
when <code>PossibleValue::help</code> is present in
<code>--help</code></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="194c676f60"><code>194c676</code></a>
chore: Release</li>
<li><a
href="44838f6606"><code>44838f6</code></a>
docs: Update changelog</li>
<li><a
href="0f59d55ff6"><code>0f59d55</code></a>
Merge pull request <a
href="https://redirect.github.com/clap-rs/clap/issues/6027">#6027</a>
from Alpha1337k/master</li>
<li><a
href="e2aa2f07d1"><code>e2aa2f0</code></a>
Feat: Add catch-all on external subcommands for zsh</li>
<li><a
href="b9c0aee9f2"><code>b9c0aee</code></a>
Feat: Add external subcommands test to suite</li>
<li>See full diff in <a
href="https://github.com/clap-rs/clap/compare/clap_complete-v4.5.53...clap_complete-v4.5.54">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.53&new-version=4.5.54)](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>
2026-01-05 23:07:55 +03:00
Colton Loftus
53d925a8ab docs: Add Documentation on the Map Projection / CRS (#2324) 2026-01-04 21:31:10 -08:00
0xferrous
1e0ab0c549 feat(ratatui-crossterm): add IntoCrossterm<ContentStyle> for Style (#2323) 2026-01-04 01:57:29 -08:00
Orhun Parmaksız
e82b3b77fe docs(buffer): run the doctests for Buffer (#2319)
addresses
https://github.com/ratatui/ratatui/pull/2314#pullrequestreview-3621576941

the doctest was not running at all... (due to `fn foo`)
2026-01-01 23:59:26 +03:00
Josh McKinney
b696ea37b2 test(core): Split Terminal into submodules and expand test coverage (#2315)
- Split the `Terminal` implementation into focused submodules to improve
readability and maintainability.
- Add characterization tests covering `Terminal` initialization, buffer
lifecycle, resizing and autoresize behavior, and rendering paths.
- Add inline viewport tests for `compute_inline_size` and
`insert_before` in both fallback and scrolling-regions modes, including
an end-to-end `draw -> insert_before -> draw scenario` scenario.
- Extend `TestBackend` cursor plumbing to support the new terminal tests
and assert cursor/ behavior.
2026-01-01 11:46:33 -08:00
Josh McKinney
64d964b259 docs: Update Terminal docs (#2312)
## Summary

- Improve `Terminal` Rustdocs to better explain typical app setup, the
rendering pipeline, and how
diff-based rendering works (including full redraw behavior on viewport
size changes).
- Clarify viewport concepts and behavior in `Viewport`/`Terminal` docs,
with a more complete inline
  section (anchoring, scrolling, resize behavior).
- Reorder `Terminal::draw` / `Terminal::try_draw` closer to constructors
so the primary rendering
  entry points are easier to find.

## Notes

- Docs-only change; no public API or runtime behavior changes.
- No new tests in this PR.

## Test Plan

- `cargo +nightly fmt`
- `cargo +nightly docs-rs -p ratatui-core`
- `cargo test -p ratatui-core --doc --features std`
2026-01-01 11:15:55 -08:00
homebrewmellow
8d73d4738e docs: Fix comment to reflect correct symbol in assertion (#2314) 2025-12-31 18:22:41 -08:00
n4n5
556cc7b543 feat: add comment for inner area to popup example (#2309) 2025-12-31 18:21:04 -08:00
cgzones
fbd562117b docs: fix misspellings (#2310) 2025-12-30 18:36:14 +03:00
Alan Somers
d2b0ce17a4 fix: Fix the dependency on time (#2306)
ratatui-widgets uses time's Month::length(), which requires time-0.3.37
or later.
2025-12-29 12:38:09 -08:00
Josh McKinney
01a15f9809 feat: Add AsRef impls for widget types (#2297) 2025-12-29 11:16:27 -08:00
dependabot[bot]
1c9f56aa4b build(deps): bump octocrab from 0.49.2 to 0.49.4 (#2305)
Bumps [octocrab](https://github.com/XAMPPRocky/octocrab) from 0.49.2 to
0.49.4.
<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.49.4</h2>
<h3>Added</h3>
<ul>
<li>Add squash_merge_commit_title, squash_merge_commit_title to repo
model (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/845">#845</a>)</li>
</ul>
<h2>v0.49.3</h2>
<h3>Added</h3>
<ul>
<li>Http caching &amp; Conditional requests (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/831">#831</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.49.3...v0.49.4">0.49.4</a>
- 2025-12-25</h2>
<h3>Added</h3>
<ul>
<li>Add squash_merge_commit_title, squash_merge_commit_title to repo
model (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/845">#845</a>)</li>
</ul>
<h2><a
href="https://github.com/XAMPPRocky/octocrab/compare/v0.49.2...v0.49.3">0.49.3</a>
- 2025-12-21</h2>
<h3>Added</h3>
<ul>
<li>Http caching &amp; Conditional requests (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/pull/831">#831</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="4ffda40672"><code>4ffda40</code></a>
chore: release v0.49.4 (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/846">#846</a>)</li>
<li><a
href="0c20a918d6"><code>0c20a91</code></a>
feat: Add squash_merge_commit_title, squash_merge_commit_title to repo
model ...</li>
<li><a
href="73799b212a"><code>73799b2</code></a>
chore: release v0.49.3 (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/844">#844</a>)</li>
<li><a
href="c08ad2e894"><code>c08ad2e</code></a>
feat: Http caching &amp; Conditional requests (<a
href="https://redirect.github.com/XAMPPRocky/octocrab/issues/831">#831</a>)</li>
<li>See full diff in <a
href="https://github.com/XAMPPRocky/octocrab/compare/v0.49.2...v0.49.4">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.49.2&new-version=0.49.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>
2025-12-29 18:40:21 +03:00
dependabot[bot]
0e89e042e0 build(deps): bump serde_json from 1.0.146 to 1.0.148 (#2304)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.146 to
1.0.148.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/serde-rs/json/releases">serde_json's
releases</a>.</em></p>
<blockquote>
<h2>v1.0.148</h2>
<ul>
<li>Update <code>zmij</code> dependency to 1.0</li>
</ul>
<h2>v1.0.147</h2>
<ul>
<li>Switch float-to-string algorithm from Ryū to Żmij for better f32 and
f64 serialization performance (<a
href="https://redirect.github.com/serde-rs/json/issues/1304">#1304</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="8b291c4c56"><code>8b291c4</code></a>
Release 1.0.148</li>
<li><a
href="1aefe15273"><code>1aefe15</code></a>
Update to zmij 1.0</li>
<li><a
href="62d6e8d615"><code>62d6e8d</code></a>
Release 1.0.147</li>
<li><a
href="fd829a65be"><code>fd829a6</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/json/issues/1304">#1304</a>
from dtolnay/zmij</li>
<li><a
href="e757a3d881"><code>e757a3d</code></a>
Switch from ryu -&gt; zmij for float formatting</li>
<li>See full diff in <a
href="https://github.com/serde-rs/json/compare/v1.0.146...v1.0.148">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=serde_json&package-manager=cargo&previous-version=1.0.146&new-version=1.0.148)](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>
2025-12-29 18:39:32 +03:00
dependabot[bot]
2e73be7982 build(deps): bump taiki-e/install-action from 2.65.1 to 2.65.7 (#2303)
Bumps
[taiki-e/install-action](https://github.com/taiki-e/install-action) from
2.65.1 to 2.65.7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/taiki-e/install-action/releases">taiki-e/install-action's
releases</a>.</em></p>
<blockquote>
<h2>2.65.7</h2>
<ul>
<li>
<p>Update <code>cargo-no-dev-deps@latest</code> to 0.2.19.</p>
</li>
<li>
<p>Update <code>cargo-minimal-versions@latest</code> to 0.1.34.</p>
</li>
<li>
<p>Update <code>cargo-insta@latest</code> to 1.45.1.</p>
</li>
<li>
<p>Update <code>cargo-hack@latest</code> to 0.6.40.</p>
</li>
<li>
<p>Update <code>dprint@latest</code> to 0.51.1.</p>
</li>
</ul>
<h2>2.65.6</h2>
<ul>
<li>
<p>Update <code>dprint@latest</code> to 0.51.0.</p>
</li>
<li>
<p>Update <code>vacuum@latest</code> to 0.22.0.</p>
</li>
</ul>
<h2>2.65.5</h2>
<ul>
<li>
<p>Update <code>tombi@latest</code> to 0.7.12.</p>
</li>
<li>
<p>Update <code>cargo-binstall@latest</code> to 1.16.6.</p>
</li>
</ul>
<h2>2.65.4</h2>
<ul>
<li>
<p>Update <code>cargo-nextest@latest</code> to 0.9.116.</p>
</li>
<li>
<p>Update <code>prek@latest</code> to 0.2.25.</p>
</li>
</ul>
<h2>2.65.3</h2>
<ul>
<li>Update <code>tombi@latest</code> to 0.7.11.</li>
</ul>
<h2>2.65.2</h2>
<ul>
<li>
<p>Update <code>prek@latest</code> to 0.2.24.</p>
</li>
<li>
<p>Update <code>wasmtime@latest</code> to 40.0.0.</p>
</li>
<li>
<p>Update <code>vacuum@latest</code> to 0.21.7.</p>
</li>
<li>
<p>Update <code>tombi@latest</code> to 0.7.10.</p>
</li>
<li>
<p>Update <code>syft@latest</code> to 1.39.0.</p>
</li>
<li>
<p>Update <code>cargo-binstall@latest</code> to 1.16.5.</p>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md">taiki-e/install-action's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<p>All notable changes to this project will be documented in this
file.</p>
<p>This project adheres to <a href="https://semver.org">Semantic
Versioning</a>.</p>
<!-- raw HTML omitted -->
<h2>[Unreleased]</h2>
<ul>
<li>Update <code>tombi@latest</code> to 0.7.13.</li>
</ul>
<h2>[2.65.7] - 2025-12-29</h2>
<ul>
<li>
<p>Update <code>cargo-no-dev-deps@latest</code> to 0.2.19.</p>
</li>
<li>
<p>Update <code>cargo-minimal-versions@latest</code> to 0.1.34.</p>
</li>
<li>
<p>Update <code>cargo-insta@latest</code> to 1.45.1.</p>
</li>
<li>
<p>Update <code>cargo-hack@latest</code> to 0.6.40.</p>
</li>
<li>
<p>Update <code>dprint@latest</code> to 0.51.1.</p>
</li>
</ul>
<h2>[2.65.6] - 2025-12-28</h2>
<ul>
<li>
<p>Update <code>dprint@latest</code> to 0.51.0.</p>
</li>
<li>
<p>Update <code>vacuum@latest</code> to 0.22.0.</p>
</li>
</ul>
<h2>[2.65.5] - 2025-12-27</h2>
<ul>
<li>
<p>Update <code>tombi@latest</code> to 0.7.12.</p>
</li>
<li>
<p>Update <code>cargo-binstall@latest</code> to 1.16.6.</p>
</li>
</ul>
<h2>[2.65.4] - 2025-12-27</h2>
<ul>
<li>
<p>Update <code>cargo-nextest@latest</code> to 0.9.116.</p>
</li>
<li>
<p>Update <code>prek@latest</code> to 0.2.25.</p>
</li>
</ul>
<h2>[2.65.3] - 2025-12-26</h2>
<ul>
<li>Update <code>tombi@latest</code> to 0.7.11.</li>
</ul>
<h2>[2.65.2] - 2025-12-23</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="4c6723ec9c"><code>4c6723e</code></a>
Release 2.65.7</li>
<li><a
href="9ff15877d9"><code>9ff1587</code></a>
Update <code>cargo-no-dev-deps@latest</code> to 0.2.19</li>
<li><a
href="4f0419fae3"><code>4f0419f</code></a>
Update <code>cargo-minimal-versions@latest</code> to 0.1.34</li>
<li><a
href="1eecdc5eb1"><code>1eecdc5</code></a>
Update <code>cargo-insta@latest</code> to 1.45.1</li>
<li><a
href="cff8e9966c"><code>cff8e99</code></a>
Update <code>cargo-hack@latest</code> to 0.6.40</li>
<li><a
href="080e4ee4f5"><code>080e4ee</code></a>
Update <code>dprint@latest</code> to 0.51.1</li>
<li><a
href="28a9d316db"><code>28a9d31</code></a>
Release 2.65.6</li>
<li><a
href="323c4aadcd"><code>323c4aa</code></a>
Update <code>dprint@latest</code> to 0.51.0</li>
<li><a
href="bfbd3b229c"><code>bfbd3b2</code></a>
Update <code>vacuum@latest</code> to 0.22.0</li>
<li><a
href="45a93d9c71"><code>45a93d9</code></a>
Release 2.65.5</li>
<li>Additional commits viewable in <a
href="b9c5db3aef...4c6723ec9c">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=taiki-e/install-action&package-manager=github_actions&previous-version=2.65.1&new-version=2.65.7)](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>
2025-12-29 18:39:08 +03:00
Kareem Khazem
f9d066f4d7 feat(table): let Cells span multiple columns (#2150)
Add a 'column_span' field to table cells. The default value
is 1; larger values will cause cells to span over multiple columns,
being rendered over all columns plus the spaces between them.

Fixes #1568.
2025-12-28 21:25:42 -08:00
Josh McKinney
65c520245a fix(changelog): fix typo (#2300) 2025-12-27 22:19:39 +03:00
Orhun Parmaksız
f8b42adb5f docs(breaking-changes): update header for 0.30.0 (#2295) 2025-12-26 13:20:21 +03:00
34 changed files with 4211 additions and 1075 deletions

View File

@@ -13,27 +13,43 @@ A detailed and complete issue is more likely to be processed quickly.
-->
## Description
<!--
A clear and concise description of what the bug is.
-->
## To Reproduce
<!--
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.
-->
## Are you willing to contribute a fix?
<!--
If you would like to work on a fix, check one of the boxes below. Maintainers can help point
you to the right place in the codebase.
-->
- [ ] I am willing to open a PR for this bug.
- [ ] I can try to investigate, but I will need guidance.
- [ ] I am not able to work on a fix right now.
## Environment
<!--
Add a description of the systems where you are observing the issue. For example:
- OS: Linux
@@ -50,6 +66,7 @@ Add a description of the systems where you are observing the issue. For example:
- Backend:
## Additional context
<!--
Add any other context about the problem here.
If you already looked into the issue, include all the leads you have explored.

View File

@@ -8,11 +8,13 @@ assignees: ''
---
## Problem
<!--
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
-->
## Solution
<!--
A clear and concise description of what you want to happen.
Things to consider:
@@ -22,11 +24,24 @@ Things to consider:
-->
## Alternatives
<!--
A clear and concise description of any alternative solutions or features you've considered.
-->
## Are you willing to contribute an implementation?
<!--
If you would like to work on this, check one of the boxes below. Maintainers can help refine
the scope and discuss approach.
-->
- [ ] I am willing to open a PR implementing this.
- [ ] I can try to implement it, but I will need guidance.
- [ ] I am not able to implement this right now.
## Additional context
<!--
Add any other context or screenshots about the feature request here.
-->

View File

@@ -37,7 +37,7 @@ jobs:
toolchain: nightly
components: rustfmt
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
- uses: taiki-e/install-action@b9c5db3aef04caffaf95a1d03931de10fb2a140f # v2
- uses: taiki-e/install-action@3522286d40783523f9c7880e33f785905b4c20d0 # v2
with:
tool: taplo-cli
- run: cargo xtask format --check
@@ -51,7 +51,7 @@ jobs:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06 # master
- uses: crate-ci/typos@bb4666ad77b539a6b4ce4eda7ebb6de553704021 # master
# Check for any disallowed dependencies in the codebase due to license / security issues.
# See <https://github.com/EmbarkStudios/cargo-deny>
@@ -68,7 +68,7 @@ jobs:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- uses: EmbarkStudios/cargo-deny-action@76cd80eb775d7bbbd2d80292136d74d39e1b4918 # v2
- uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2
with:
rust-toolchain: stable
log-level: info
@@ -139,7 +139,7 @@ jobs:
with:
toolchain: stable
components: llvm-tools
- uses: taiki-e/install-action@b9c5db3aef04caffaf95a1d03931de10fb2a140f # v2
- uses: taiki-e/install-action@3522286d40783523f9c7880e33f785905b4c20d0 # v2
with:
tool: cargo-llvm-cov
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
@@ -165,7 +165,7 @@ jobs:
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
with:
toolchain: ${{ matrix.toolchain }}
- uses: taiki-e/install-action@b9c5db3aef04caffaf95a1d03931de10fb2a140f # v2
- uses: taiki-e/install-action@3522286d40783523f9c7880e33f785905b4c20d0 # v2
with:
tool: cargo-hack
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
@@ -204,7 +204,7 @@ jobs:
with:
persist-credentials: false
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
- uses: taiki-e/install-action@b9c5db3aef04caffaf95a1d03931de10fb2a140f # v2
- uses: taiki-e/install-action@3522286d40783523f9c7880e33f785905b4c20d0 # v2
with:
tool: cargo-rdme
- run: cargo xtask readme --check
@@ -226,7 +226,7 @@ jobs:
- uses: dtolnay/install@74f735cdf643820234e37ae1c4089a08fd266d8a # master
with:
crate: cargo-docs-rs
- uses: taiki-e/install-action@b9c5db3aef04caffaf95a1d03931de10fb2a140f # v2
- uses: taiki-e/install-action@3522286d40783523f9c7880e33f785905b4c20d0 # v2
with:
tool: cargo-hack
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
@@ -244,7 +244,7 @@ jobs:
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
with:
toolchain: stable
- uses: taiki-e/install-action@b9c5db3aef04caffaf95a1d03931de10fb2a140f # v2
- uses: taiki-e/install-action@3522286d40783523f9c7880e33f785905b4c20d0 # v2
with:
tool: cargo-hack
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
@@ -265,7 +265,7 @@ jobs:
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
with:
toolchain: stable
- uses: taiki-e/install-action@b9c5db3aef04caffaf95a1d03931de10fb2a140f # v2
- uses: taiki-e/install-action@3522286d40783523f9c7880e33f785905b4c20d0 # v2
with:
tool: cargo-hack
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2

View File

@@ -10,7 +10,9 @@ GitHub with a [breaking change] label.
This is a quick summary of the sections below:
- [v0.30.0 Unreleased](#v0300-unreleased)
- [v0.30.1](#v0301)
- Adding `AsRef` impls for widgets may affect type inference in rare cases
- [v0.30.0](#v0300)
- `Flex::SpaceAround` now mirrors flexbox: space between items is twice the size of the outer gaps
are twice the size of first and last elements
- `block::Title` no longer exists
@@ -93,7 +95,18 @@ This is a quick summary of the sections below:
- MSRV is now 1.63.0
- `List` no longer ignores empty strings
## v0.30.0 Unreleased
## [v0.30.1](https://github.com/ratatui/ratatui/releases/tag/ratatui-v0.30.1)
### Adding `AsRef` impls for widgets may affect type inference ([#2297])
[#2297]: https://github.com/ratatui/ratatui/pull/2297
Adding `AsRef<Self>` for built-in widgets can change type inference outcomes in rare cases where
`AsRef` is part of a trait bound, and can also conflict with downstream blanket or manual `AsRef`
impls for widget types. If you hit new ambiguity errors, add explicit type annotations or specify
the concrete widget type to guide inference, and remove any redundant `AsRef` impls.
## [v0.30.0](https://github.com/ratatui/ratatui/releases/tag/ratatui-v0.30.0)
### `Marker` is now non-exhaustive ([#2236])

View File

@@ -340,7 +340,7 @@ We are excited to announce the biggest release of `ratatui` so far - a Rust libr
- [22610b0](https://github.com/ratatui/ratatui/commit/22610b019b9e7b451cd2ba2c44aa625fd24a8f95) *(uncategorized)* Support adding an Offset to Position by @joshka in [#2239](https://github.com/ratatui/ratatui/pull/2239)
> Adds Position::offset() and arithmentic ops (Position + Offset and
> Adds Position::offset() and arithmetic ops (Position + Offset and
> Position - Offset)
>
> Fixes:https://github.com/ratatui/ratatui/issues/2018

48
Cargo.lock generated
View File

@@ -417,9 +417,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.53"
version = "4.5.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
dependencies = [
"clap_builder",
"clap_derive",
@@ -437,9 +437,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.53"
version = "4.5.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
dependencies = [
"anstream",
"anstyle",
@@ -1028,7 +1028,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
dependencies = [
"libc",
"windows-sys 0.60.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -1865,9 +1865,9 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "lru"
version = "0.16.2"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96051b46fc183dc9cd4a223960ef37b9af631b55191852a8274bfef064cda20f"
checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
dependencies = [
"hashbrown 0.16.1",
]
@@ -2099,9 +2099,9 @@ dependencies = [
[[package]]
name = "octocrab"
version = "0.49.2"
version = "0.49.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a05f533ce747c92b413d143ae258c3bd11a56ffdf7f2cee1896539c89c59acc"
checksum = "89f6f72d7084a80bf261bb6b6f83bd633323d5633d5ec7988c6c95b20448b2b5"
dependencies = [
"arc-swap",
"async-trait",
@@ -2871,9 +2871,9 @@ dependencies = [
[[package]]
name = "rsa"
version = "0.9.9"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88"
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
dependencies = [
"const-oid",
"digest",
@@ -2943,7 +2943,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -2956,7 +2956,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.9.4",
"windows-sys 0.60.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -3138,15 +3138,15 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.146"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "217ca874ae0207aac254aa02c957ded05585a90892cc8d87f9e5fa49669dadd8"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
"zmij",
]
[[package]]
@@ -3659,9 +3659,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.48.0"
version = "1.49.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
dependencies = [
"libc",
"mio",
@@ -3694,9 +3694,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
version = "0.1.17"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
dependencies = [
"futures-core",
"pin-project-lite",
@@ -4289,7 +4289,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -4673,3 +4673,9 @@ dependencies = [
"quote",
"syn 2.0.106",
]
[[package]]
name = "zmij"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f4a4e8e9dc5c62d159f04fcdbe07f4c3fb710415aab4754bf11505501e3251d"

View File

@@ -64,7 +64,7 @@ strum = { version = "0.27", default-features = false, features = ["derive"] }
termion = "4"
termwiz = "0.23"
thiserror = { version = "2", default-features = false }
time = { version = "0.3", default-features = false }
time = { version = "0.3.37", default-features = false }
tokio = "1"
tokio-stream = "0.1"
tracing = "0.1"

View File

@@ -34,7 +34,7 @@ body = """
{% macro commit(commit) -%}
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui/ratatui/commit/" ~ commit.id }}) \
*({{commit.scope | default(value = "uncategorized") | lower }})* {{ commit.message | upper_first | trim }}\
{% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}\
{% if commit.remote.username %} by `@{{ commit.remote.username }}`{%- endif -%}\
{% if commit.remote.pr_number %} in [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}){%- endif %}\
{%- if commit.breaking %} [**breaking**]{% endif %}
{%- if commit.body %}\n\n{{ commit.body | indent(prefix=" > ", first=true, blank=true) }}
@@ -123,6 +123,7 @@ commit_preprocessors = [
{ pattern = '\<[f]eatuers\>', replace = "features" },
{ pattern = '\<[s]pecically\>', replace = "specially" },
{ pattern = '\<[g]ague\>', replace = "gauge" },
{ pattern = '\<[a]rithmentic\>', replace = "arithmetic" },
{ pattern = '\<[i]ntructions\>', replace = "instructions" },
{ pattern = '\<[i]mplementated\>', replace = "implemented" },
]

View File

@@ -1,3 +1,5 @@
use std::cmp::Ordering;
/// A Ratatui example that demonstrates how different layout constraints work.
///
/// It also supports swapping constraints, adding and removing blocks, and changing the spacing
@@ -30,7 +32,7 @@ fn main() -> Result<()> {
#[derive(Default)]
struct App {
mode: AppMode,
spacing: u16,
spacing: i16,
constraints: Vec<Constraint>,
selected_index: usize,
value: u16,
@@ -269,7 +271,7 @@ impl App {
}
fn instructions() -> impl Widget {
let text = "◄ ►: select, ▲ ▼: edit, 1-6: swap, a: add, x: delete, q: quit, + -: spacing";
let text = "◄ ►: select, ▲ ▼: edit, 1-6: swap, a: add, x: delete, q: quit, +/-: spacing";
Paragraph::new(text)
.fg(Self::TEXT_COLOR)
.centered()
@@ -307,10 +309,12 @@ impl App {
///
/// Only shows the gap when spacing is not zero
fn axis(&self, width: u16) -> impl Widget {
let label = if self.spacing != 0 {
format!("{} px (gap: {} px)", width, self.spacing)
} else {
format!("{width} px")
let label = match self.spacing.cmp(&0) {
Ordering::Greater => format!("{width} px (gap: {} px)", self.spacing),
Ordering::Less => {
format!("{width} px (overlap: {} px)", self.spacing.unsigned_abs())
}
Ordering::Equal => format!("{width} px"),
};
let bar_width = width.saturating_sub(2) as usize; // we want to `<` and `>` at the ends
let width_bar = format!("<{label:-^bar_width$}>");

View File

@@ -11,10 +11,10 @@
use color_eyre::Result;
use crossterm::event::{self, KeyCode};
use ratatui::Frame;
use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::layout::{Constraint, Layout};
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::widgets::{Block, Clear};
use ratatui::widgets::{Block, Clear, Paragraph};
fn main() -> Result<()> {
color_eyre::install()?;
@@ -52,19 +52,14 @@ fn render(frame: &mut Frame, show_popup: bool) {
frame.render_widget(Block::bordered().title("Content").on_blue(), content);
if show_popup {
let popup = Block::bordered().title("Popup");
let popup_area = centered_area(area, 60, 20);
let popup_block = Block::bordered().title("Popup");
let centered_area = area.centered(Constraint::Percentage(60), Constraint::Percentage(20));
// clears out any background in the area before rendering the popup
frame.render_widget(Clear, popup_area);
frame.render_widget(popup, popup_area);
frame.render_widget(Clear, centered_area);
let paragraph = Paragraph::new("Lorem ipsum").block(popup_block);
frame.render_widget(paragraph, centered_area);
// another solution is to use the inner area of the block
// let inner_area = popup_block.inner(centered_area);
// frame.render_widget(your_widget, inner_area);
}
}
/// Create a centered rect using up certain percentage of the available rect
fn centered_area(area: Rect, percent_x: u16, percent_y: u16) -> Rect {
let vertical = Layout::vertical([Constraint::Percentage(percent_y)]).flex(Flex::Center);
let horizontal = Layout::horizontal([Constraint::Percentage(percent_x)]).flex(Flex::Center);
let [area] = area.layout(&vertical);
let [area] = area.layout(&horizontal);
area
}

View File

@@ -222,6 +222,19 @@ impl App {
let item = data.ref_array();
item.into_iter()
.map(|content| Cell::from(Text::from(format!("\n{content}\n"))))
.enumerate()
.map(|(idx, cell)| {
if i == 3 && idx == 1 {
Cell::from(Text::from(
// Gratuitously long error message to demonstrate column_span(2)
"\n[no address or email address is available for this person]\n"
.to_string(),
))
.column_span(2)
} else {
cell
}
})
.collect::<Row>()
.style(Style::new().fg(self.colors.row_fg).bg(color))
.height(4)

View File

@@ -109,19 +109,28 @@ use crate::layout::{Position, Size};
mod test;
pub use self::test::TestBackend;
/// Enum representing the different types of clearing operations that can be performed
/// on the terminal screen.
/// Defines which region of the terminal's visible display area is cleared.
///
/// Clearing operates on character cells in the active display surface. It does not move, hide, or
/// reset the cursor position. If the cursor lies inside the cleared region, the character cell at
/// the cursor position is cleared as well.
///
/// Clearing applies to the terminal's visible display area, not just content previously drawn by
/// Ratatui. No guarantees are made about scrollback, history, or off-screen buffers.
#[derive(Debug, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum ClearType {
/// Clear the entire screen.
/// Clears all character cells in the visible display area.
All,
/// Clear everything after the cursor.
/// Clears all character cells from the cursor position (inclusive) through the end of the
/// display area.
AfterCursor,
/// Clear everything before the cursor.
/// Clears all character cells from the start of the display area through the cursor position
/// (inclusive).
BeforeCursor,
/// Clear the current line.
/// Clears all character cells in the cursor's current line.
CurrentLine,
/// Clear everything from the cursor until the next newline.
/// Clears all character cells from the cursor position (inclusive) to the end of the current
/// line.
UntilNewLine,
}
@@ -237,7 +246,14 @@ pub trait Backend {
self.set_cursor_position(Position { x, y })
}
/// Clears the whole terminal screen
/// Clears all character cells in the terminal's visible display area.
///
/// This operation preserves the cursor position. If the cursor lies within the cleared
/// region, the character cell at the cursor position is cleared. No guarantees are made about
/// scrollback, history, or off-screen buffers.
///
/// This is equivalent to calling [`clear_region`](Self::clear_region) with
/// [`ClearType::All`].
///
/// # Example
///
@@ -251,7 +267,13 @@ pub trait Backend {
/// ```
fn clear(&mut self) -> Result<(), Self::Error>;
/// Clears a specific region of the terminal specified by the [`ClearType`] parameter
/// Clears a specific region of the terminal's visible display area, as defined by
/// [`ClearType`].
///
/// This operation preserves the cursor position. If the cursor lies within the cleared
/// region, the character cell at the cursor position is cleared. Clearing applies to the
/// active display surface only and does not make guarantees about scrollback, history, or
/// off-screen buffers.
///
/// This method is optional and may not be implemented by all backends. The default
/// implementation calls [`clear`] if the `clear_type` is [`ClearType::All`] and returns an

View File

@@ -105,6 +105,19 @@ impl TestBackend {
&self.buffer
}
/// Returns whether the cursor is visible.
pub const fn cursor_visible(&self) -> bool {
self.cursor
}
/// Returns the current cursor position.
pub const fn cursor_position(&self) -> Position {
Position {
x: self.pos.0,
y: self.pos.1,
}
}
/// Returns a reference to the internal scrollback buffer of the `TestBackend`.
///
/// The scrollback buffer represents the part of the screen that is currently hidden from view,
@@ -275,12 +288,12 @@ impl Backend for TestBackend {
let region = match clear_type {
ClearType::All => return self.clear(),
ClearType::AfterCursor => {
let index = self.buffer.index_of(self.pos.0, self.pos.1) + 1;
let index = self.buffer.index_of(self.pos.0, self.pos.1);
&mut self.buffer.content[index..]
}
ClearType::BeforeCursor => {
let index = self.buffer.index_of(self.pos.0, self.pos.1);
&mut self.buffer.content[..index]
&mut self.buffer.content[..=index]
}
ClearType::CurrentLine => {
let line_start_index = self.buffer.index_of(0, self.pos.1);
@@ -620,7 +633,7 @@ mod tests {
backend.assert_buffer_lines([
"aaaaaaaaaa",
"aaaaaaaaaa",
"aaaa ",
"aaa ",
" ",
" ",
]);
@@ -644,7 +657,7 @@ mod tests {
" ",
" ",
" ",
" aaaaa",
" aaaa",
"aaaaaaaaaa",
]);
}

View File

@@ -25,7 +25,7 @@ use crate::text::{Line, Span};
/// use ratatui_core::layout::{Position, Rect};
/// use ratatui_core::style::{Color, Style};
///
/// # fn foo() -> Option<()> {
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut buf = Buffer::empty(Rect {
/// x: 0,
/// y: 0,
@@ -39,13 +39,15 @@ use crate::text::{Line, Span};
///
/// // indexing using (x, y) tuple (which is converted to Position)
/// buf[(0, 1)].set_symbol("B");
/// assert_eq!(buf[(0, 1)].symbol(), "x");
/// assert_eq!(buf[(0, 1)].symbol(), "B");
///
/// // getting an Option instead of panicking if the position is outside the buffer
/// let cell = buf.cell_mut(Position { x: 0, y: 2 })?;
/// let cell = buf
/// .cell_mut(Position { x: 0, y: 2 })
/// .ok_or("cell not found")?;
/// cell.set_symbol("C");
///
/// let cell = buf.cell(Position { x: 0, y: 2 })?;
/// let cell = buf.cell(Position { x: 0, y: 2 }).ok_or("cell not found")?;
/// assert_eq!(cell.symbol(), "C");
///
/// buf.set_string(
@@ -58,7 +60,7 @@ use crate::text::{Line, Span};
/// assert_eq!(cell.symbol(), "r");
/// assert_eq!(cell.fg, Color::Red);
/// assert_eq!(cell.bg, Color::White);
/// # Some(())
/// # Ok(())
/// # }
/// ```
#[derive(Default, Clone, Eq, PartialEq, Hash)]

View File

@@ -31,10 +31,381 @@
//! [`Backend`]: crate::backend::Backend
//! [`Buffer`]: crate::buffer::Buffer
mod backend;
mod buffers;
mod cursor;
mod frame;
mod terminal;
mod init;
mod inline;
mod render;
mod resize;
mod viewport;
pub use frame::{CompletedFrame, Frame};
pub use terminal::{Options as TerminalOptions, Terminal};
pub use viewport::Viewport;
use crate::backend::Backend;
use crate::buffer::Buffer;
use crate::layout::{Position, Rect};
/// An interface to interact and draw [`Frame`]s on the user's terminal.
///
/// This is the main entry point for Ratatui. It is responsible for drawing and maintaining the
/// state of the buffers, cursor and viewport.
///
/// If you're building a fullscreen application with the `ratatui` crate's default backend
/// ([Crossterm]), prefer [`ratatui::run`] (or [`ratatui::init`] + [`ratatui::restore`]) over
/// constructing `Terminal` directly. These helpers enable common terminal modes (raw mode +
/// alternate screen) and restore them on exit and on panic.
///
/// ```rust,ignore
/// ratatui::run(|terminal| {
/// let mut should_quit = false;
/// while !should_quit {
/// terminal.draw(|frame| {
/// frame.render_widget("Hello, World!", frame.area());
/// })?;
///
/// // Handle events, update application state, and set `should_quit = true` to exit.
/// }
/// Ok(())
/// })?;
/// ```
///
/// # Typical Usage
///
/// In a typical application, the flow is: set up a terminal, run an event loop, update state, and
/// draw each frame.
///
/// 1. Choose a setup path for a `Terminal`. Most apps call [`ratatui::run`], which passes a
/// preconfigured `Terminal` into your callback. If you need more control, use [`ratatui::init`]
/// and [`ratatui::restore`], or construct a `Terminal` manually via [`Terminal::new`]
/// (fullscreen) or [`Terminal::with_options`] (select a [`Viewport`]).
/// 2. Enter your application's event loop and call [`Terminal::draw`] (or [`Terminal::try_draw`])
/// to render the current UI state into a [`Frame`].
/// 3. Handle input and application state updates between draw calls.
/// 4. If the terminal is resized, call [`Terminal::draw`] again. Ratatui automatically resizes
/// fullscreen and inline viewports during `draw`; fixed viewports require an explicit call to
/// [`Terminal::resize`] if you want the region to change.
///
/// # Rendering Pipeline
///
/// A single call to [`Terminal::draw`] (or [`Terminal::try_draw`]) represents one render pass. In
/// broad strokes, Ratatui:
///
/// 1. Checks whether the underlying terminal size changed (see [`Terminal::autoresize`]).
/// 2. Creates a [`Frame`] backed by the current buffer (see [`Terminal::get_frame`]).
/// 3. Runs your render callback to populate that buffer.
/// 4. Diffs the current buffer against the previous buffer and writes the changes (see
/// [`Terminal::flush`]).
/// 5. Applies cursor visibility and position requested by the frame (see
/// [`Frame::set_cursor_position`]).
/// 6. Swaps the buffers to prepare for the next render pass (see [`Terminal::swap_buffers`]).
/// 7. Flushes the backend (see [`Backend::flush`]).
///
/// Each render pass starts with an empty buffer for the current viewport. Your render callback
/// should render everything that should be visible in [`Frame::area`], even if it is unchanged
/// from the previous frame. Ratatui diffs the current and previous buffers and only writes the
/// changes; anything you don't render is treated as empty and may clear previously drawn content.
///
/// If the viewport size changes between render passes (for example via [`Terminal::autoresize`] or
/// an explicit [`Terminal::resize`]), Ratatui clears the viewport and resets the previous buffer so
/// the next `draw` is treated as a full redraw.
///
/// Most applications should use [`Terminal::draw`] / [`Terminal::try_draw`]. For manual rendering
/// (primarily for tests), you can build a frame with [`Terminal::get_frame`], write diffs with
/// [`Terminal::flush`], then call [`Terminal::swap_buffers`]. If your backend buffers output, also
/// call [`Backend::flush`].
///
/// ```rust,no_run
/// # mod ratatui {
/// # pub use ratatui_core::backend;
/// # pub use ratatui_core::terminal::Terminal;
/// # }
/// use ratatui::Terminal;
/// use ratatui::backend::{Backend, TestBackend};
///
/// let backend = TestBackend::new(10, 10);
/// let mut terminal = Terminal::new(backend)?;
///
/// // Manual render pass (roughly what `Terminal::draw` does internally).
/// {
/// let mut frame = terminal.get_frame();
/// frame.render_widget("Hello World!", frame.area());
/// }
///
/// terminal.flush()?;
/// terminal.swap_buffers();
/// terminal.backend_mut().flush()?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// # Viewports
///
/// The viewport controls *where* Ratatui draws and therefore what [`Frame::area`] represents.
/// Most applications use [`Viewport::Fullscreen`], but Ratatui also supports [`Viewport::Inline`]
/// and [`Viewport::Fixed`].
///
/// Choose a viewport at initialization time with [`Terminal::with_options`] and
/// [`TerminalOptions`].
///
/// In [`Viewport::Fullscreen`], the viewport is the entire terminal and `Frame::area` starts at
/// (0, 0). Ratatui automatically resizes the internal buffers when the terminal size changes.
///
/// In [`Viewport::Fixed`], the viewport is a user-provided [`Rect`] in terminal coordinates.
/// `Frame::area` is that exact rectangle (including its `x`/`y` offset). Fixed viewports are not
/// automatically resized; if the region should change, call [`Terminal::resize`].
///
/// In [`Viewport::Inline`], Ratatui draws into a rectangle anchored to where the UI started. This
/// mode is described in more detail in the "Inline Viewport" section below.
///
/// ```rust,ignore
/// use ratatui::{layout::Rect, Terminal, TerminalOptions, Viewport};
/// use ratatui::backend::CrosstermBackend;
///
/// // Fullscreen (most common):
/// let fullscreen = Terminal::new(CrosstermBackend::new(std::io::stdout()))?;
///
/// // Fixed region (your app manages the coordinates):
/// let viewport = Viewport::Fixed(Rect::new(0, 0, 30, 10));
/// let fixed = Terminal::with_options(
/// CrosstermBackend::new(std::io::stdout()),
/// TerminalOptions { viewport },
/// )?;
/// ```
///
/// Applications should detect terminal resizes and call [`Terminal::draw`] to redraw the
/// application with the new size. This will automatically resize the internal buffers to match the
/// new size for inline and fullscreen viewports. Fixed viewports are not resized automatically.
///
/// # Inline Viewport
///
/// Inline mode is designed for applications that want to embed a UI into a larger CLI flow. In
/// [`Viewport::Inline`], Ratatui anchors the viewport to the backend cursor row at initialization
/// time and always starts drawing at column 0.
///
/// To reserve vertical space for the requested height, Ratatui may append lines. When the cursor is
/// near the bottom edge, terminals scroll; Ratatui accounts for that scrolling by shifting the
/// computed viewport origin upward so the viewport stays fully visible.
///
/// While running in inline mode, [`Terminal::insert_before`] can be used to print output above the
/// viewport without disturbing the UI.
/// When Ratatui is built with the `scrolling-regions` feature, `insert_before` can do this without
/// clearing and redrawing the viewport.
///
/// ```rust,ignore
/// use ratatui::{TerminalOptions, Viewport};
///
/// println!("Some output above the UI");
///
/// let options = TerminalOptions {
/// viewport: Viewport::Inline(10),
/// };
/// let mut terminal = ratatui::try_init_with_options(options)?;
///
/// terminal.insert_before(1, |buf| {
/// // Render a single line of output into `buf` before the UI.
/// // (For example: logs, status updates, or command output.)
/// })?;
/// ```
///
/// # More Information
///
/// - Choosing a viewport: [`Terminal::with_options`], [`TerminalOptions`], and [`Viewport`]
/// - The rendering pipeline: [`Terminal::draw`] and [`Terminal::try_draw`]
/// - Resize handling: [`Terminal::autoresize`] and [`Terminal::resize`]
/// - Manual rendering and testing: [`Terminal::get_frame`], [`Terminal::flush`], and
/// [`Terminal::swap_buffers`]
/// - Printing above an inline UI: [`Terminal::insert_before`]
///
/// # Initialization
///
/// Most interactive TUIs need process-wide terminal setup (for example: raw mode and an alternate
/// screen) and matching teardown on exit and on panic. In Ratatui, that setup lives in the
/// `ratatui` crate; `Terminal` itself focuses on rendering and does not implicitly change those
/// modes.
///
/// If you're using the `ratatui` crate with its default backend ([Crossterm]), there are three
/// common entry points:
///
/// - [`ratatui::run`]: recommended for most applications. Provides a [`ratatui::DefaultTerminal`],
/// runs your closure, and restores terminal state on exit and on panic.
/// - [`ratatui::init`] + [`ratatui::restore`]: like `run`, but you control the event loop and
/// decide when to restore.
/// - [`Terminal::new`] / [`Terminal::with_options`]: manual construction (for example: custom
/// backends such as [Termion] / [Termwiz], inline UIs, or fixed viewports). You are responsible
/// for terminal mode setup and teardown.
///
/// [`ratatui::run`] was introduced in Ratatui 0.30, so older tutorials may use `init`/`restore` or
/// manual construction.
///
/// Some applications install a custom panic hook to log a crash report, print a friendlier error,
/// or integrate with error reporting. If you do, install it before calling [`ratatui::init`] /
/// [`ratatui::run`]. Ratatui wraps the current hook so it can restore terminal state first (for
/// example: leaving the alternate screen and disabling raw mode) and then delegate to your hook.
///
/// Crossterm is cross-platform and is what most Ratatui applications use by default. Ratatui also
/// supports other backends such as [Termion] and [Termwiz], and third-party backends can integrate
/// by implementing [`Backend`].
///
/// # How it works
///
/// `Terminal` ties together a [`Backend`], a [`Viewport`], and a double-buffered diffing renderer.
/// The high-level flow is described in the "Rendering Pipeline" section above; this section focuses
/// on how that pipeline is implemented.
///
/// `Terminal` is generic over a [`Backend`] implementation and does not depend on a particular
/// terminal library. It relies on the backend to:
///
/// - report the current screen size (used by [`Terminal::autoresize`])
/// - draw cell updates (used by [`Terminal::flush`])
/// - clear regions (used by [`Terminal::clear`] and [`Terminal::resize`])
/// - move and show/hide the cursor (used by [`Terminal::try_draw`])
/// - optionally append lines (used by inline viewports and by [`Terminal::insert_before`])
///
/// ## Buffers and diffing
///
/// The `Terminal` maintains two [`Buffer`]s sized to the current viewport. During a render pass,
/// widgets draw into the "current" buffer via the [`Frame`] passed to your callback. At the end of
/// the pass, [`Terminal::flush`] diffs the current buffer against the previous buffer and sends
/// only the changed cells to the backend.
///
/// After flushing, [`Terminal::swap_buffers`] flips which buffer is considered "current" and resets
/// the next buffer. This is why each render pass starts from an empty buffer: your callback is
/// expected to fully redraw the viewport every time.
///
/// The [`CompletedFrame`] returned from [`Terminal::draw`] / [`Terminal::try_draw`] provides a
/// reference to the buffer that was just rendered, which can be useful for assertions in tests.
///
/// ## Viewport state and resizing
///
/// The active [`Viewport`] controls how the viewport area is computed:
///
/// - Fullscreen: `Frame::area` covers the full backend size.
/// - Fixed: `Frame::area` is the exact rectangle you provided in terminal coordinates.
/// - Inline: `Frame::area` is a rectangle anchored to the backend cursor row.
///
/// For fullscreen and inline viewports, [`Terminal::autoresize`] checks the backend size during
/// every render pass and calls [`Terminal::resize`] when it changes. Resizing updates the internal
/// buffer sizes and clears the affected region; it also resets the previous buffer so the next draw
/// is treated as a full redraw.
///
/// ## Cursor tracking
///
/// The cursor position requested by [`Frame::set_cursor_position`] is applied after
/// [`Terminal::flush`] so the cursor ends up on top of the rendered UI. `Terminal` also tracks a
/// "last known cursor position" as a best-effort record of where it last wrote, and uses that
/// information when recomputing inline viewports on resize.
///
/// ## Inline-specific behavior
///
/// Inline viewports reserve vertical space by calling [`Backend::append_lines`]. If the cursor is
/// close enough to the bottom edge, terminals scroll as lines are appended. Ratatui accounts for
/// that scrolling by shifting the computed viewport origin upward so the viewport remains fully
/// visible. On resize, Ratatui recomputes the inline origin while trying to keep the cursor at the
/// same relative row inside the viewport.
///
/// When Ratatui is built with the `scrolling-regions` feature, [`Terminal::insert_before`] uses
/// terminal scrolling regions to insert content above an inline viewport without clearing and
/// redrawing it.
///
/// [Crossterm]: https://crates.io/crates/crossterm
/// [Termion]: https://crates.io/crates/termion
/// [Termwiz]: https://crates.io/crates/termwiz
/// [`backend`]: crate::backend
/// [`Backend`]: crate::backend::Backend
/// [`Backend::flush`]: crate::backend::Backend::flush
/// [`Buffer`]: crate::buffer::Buffer
/// [`ratatui::DefaultTerminal`]: https://docs.rs/ratatui/latest/ratatui/type.DefaultTerminal.html
/// [`ratatui::init`]: https://docs.rs/ratatui/latest/ratatui/fn.init.html
/// [`ratatui::restore`]: https://docs.rs/ratatui/latest/ratatui/fn.restore.html
/// [`ratatui::run`]: https://docs.rs/ratatui/latest/ratatui/fn.run.html
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Terminal<B>
where
B: Backend,
{
/// The backend used to write updates to the terminal.
///
/// Most application code does not need to interact with the backend directly; see
/// [`Terminal::draw`]. Accessing the backend can be useful for backend-specific testing and
/// inspection (see [`Terminal::backend`]).
backend: B,
/// Double-buffered render state.
///
/// [`Terminal::flush`] diffs `buffers[current]` against the other buffer to compute a minimal
/// set of updates to send to the backend.
buffers: [Buffer; 2],
/// Index of the "current" buffer in [`Terminal::buffers`].
///
/// This toggles between 0 and 1 and is updated by [`Terminal::swap_buffers`].
current: usize,
/// Whether Ratatui believes it has hidden the cursor.
///
/// This is tracked so [`Drop`] can attempt to restore cursor visibility.
hidden_cursor: bool,
/// The configured [`Viewport`] mode.
///
/// This determines how the initial viewport area is computed during construction, whether
/// [`Terminal::autoresize`] runs, how [`Terminal::clear`] behaves, and whether operations like
/// [`Terminal::insert_before`] have any effect.
viewport: Viewport,
/// The current viewport rectangle in terminal coordinates.
///
/// This is the area returned by [`Frame::area`] and the size of the internal buffers. It is
/// set during construction and updated by [`Terminal::resize`]. In inline mode, calls to
/// [`Terminal::insert_before`] can also move the viewport vertically.
viewport_area: Rect,
/// Last known renderable "screen" area.
///
/// For fullscreen and inline viewports this tracks the backend-reported terminal size. For
/// fixed viewports, this tracks the user-provided fixed area.
///
/// This is used by [`Terminal::autoresize`] and is reported via [`CompletedFrame::area`].
last_known_area: Rect,
/// Last known cursor position in terminal coordinates.
///
/// This is updated when:
///
/// - [`Terminal::set_cursor_position`] is called directly.
/// - [`Frame::set_cursor_position`] is used during [`Terminal::draw`].
/// - [`Terminal::flush`] observes a diff update (used as a proxy for the "last written" cell).
///
/// Inline viewports use this during [`Terminal::resize`] to preserve the cursor's relative
/// position within the viewport.
last_known_cursor_pos: Position,
/// Number of frames rendered so far.
///
/// This increments after each successful [`Terminal::draw`] / [`Terminal::try_draw`] and wraps
/// at `usize::MAX`.
frame_count: usize,
}
/// Options to pass to [`Terminal::with_options`]
///
/// Most applications can use [`Terminal::new`]. Use `TerminalOptions` when you need to configure a
/// non-default [`Viewport`] at initialization time (see [`Terminal`] for an overview).
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct TerminalOptions {
/// Viewport used to draw to the terminal.
///
/// See [`Terminal`] for a higher-level overview, and [`Viewport`] for the per-variant
/// definition.
pub viewport: Viewport,
}
impl<B> Drop for Terminal<B>
where
B: Backend,
{
fn drop(&mut self) {
// Attempt to restore the cursor state
if self.hidden_cursor {
#[allow(unused_variables)]
if let Err(err) = self.show_cursor() {
#[cfg(feature = "std")]
std::eprintln!("Failed to show the cursor: {err}");
}
}
}
}

View File

@@ -0,0 +1,75 @@
use crate::backend::Backend;
use crate::layout::Size;
use crate::terminal::Terminal;
impl<B: Backend> Terminal<B> {
/// Returns a shared reference to the backend.
///
/// This is primarily useful for backend-specific inspection in tests (e.g. reading
/// [`TestBackend`]'s buffer). Most applications should interact with the terminal via
/// [`Terminal::draw`] rather than calling backend methods directly.
///
/// [`TestBackend`]: crate::backend::TestBackend
pub const fn backend(&self) -> &B {
&self.backend
}
/// Returns a mutable reference to the backend.
///
/// This is an advanced escape hatch. Mutating the backend directly can desynchronize Ratatui's
/// internal buffers from what's on-screen; if you do this, you may need to call
/// [`Terminal::clear`] to force a full redraw.
pub const fn backend_mut(&mut self) -> &mut B {
&mut self.backend
}
/// Queries the real size of the backend.
///
/// This returns the size of the underlying terminal. The current renderable area depends on
/// the configured [`Viewport`]; use [`Frame::area`] inside [`Terminal::draw`] if you want the
/// area you should render into.
///
/// [`Frame::area`]: crate::terminal::Frame::area
/// [`Terminal::draw`]: crate::terminal::Terminal::draw
/// [`Viewport`]: crate::terminal::Viewport
pub fn size(&self) -> Result<Size, B::Error> {
self.backend.size()
}
}
#[cfg(test)]
mod tests {
use crate::backend::TestBackend;
use crate::layout::{Position, Size};
use crate::terminal::Terminal;
#[test]
fn backend_returns_shared_reference() {
let backend = TestBackend::new(3, 2);
let terminal = Terminal::new(backend).unwrap();
assert_eq!(terminal.backend().cursor_position(), Position::ORIGIN);
}
#[test]
fn backend_mut_allows_mutating_backend_state() {
let backend = TestBackend::new(3, 2);
let mut terminal = Terminal::new(backend).unwrap();
terminal.backend_mut().resize(4, 3);
assert_eq!(terminal.size().unwrap(), Size::new(4, 3));
terminal
.backend()
.assert_buffer_lines([" ", " ", " "]);
}
#[test]
fn size_queries_underlying_backend_size() {
let mut backend = TestBackend::new(3, 2);
backend.resize(4, 3);
let terminal = Terminal::new(backend).unwrap();
assert_eq!(terminal.size().unwrap(), Size::new(4, 3));
}
}

View File

@@ -0,0 +1,380 @@
use crate::backend::{Backend, ClearType};
use crate::buffer::{Buffer, Cell};
use crate::layout::{Position, Rect};
use crate::terminal::{Frame, Terminal, Viewport};
impl<B: Backend> Terminal<B> {
/// Returns a [`Frame`] for manual rendering.
///
/// Most applications should render via [`Terminal::draw`] / [`Terminal::try_draw`]. This method
/// exposes the frame construction step used by [`Terminal::try_draw`] so tests and advanced
/// callers can render without running the full draw pipeline.
///
/// Unlike `draw` / `try_draw`, this does not call [`Terminal::autoresize`], does not write
/// updates to the backend, and does not apply any cursor changes. After rendering, you
/// typically call [`Terminal::flush`], [`Terminal::swap_buffers`], and [`Backend::flush`].
///
/// The returned `Frame` mutably borrows the current buffer, so it must be dropped before you
/// can call methods like [`Terminal::flush`]. The example below uses a scope to make that
/// explicit.
///
/// # Example
///
/// ```rust,no_run
/// # mod ratatui {
/// # pub use ratatui_core::backend;
/// # pub use ratatui_core::terminal::Terminal;
/// # }
/// use ratatui::Terminal;
/// use ratatui::backend::{Backend, TestBackend};
///
/// let backend = TestBackend::new(30, 5);
/// let mut terminal = Terminal::new(backend)?;
/// {
/// let mut frame = terminal.get_frame();
/// frame.render_widget("Hello", frame.area());
/// }
/// // When not using `draw`, present the buffer manually:
/// terminal.flush()?;
/// terminal.swap_buffers();
/// terminal.backend_mut().flush()?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// [`Backend::flush`]: crate::backend::Backend::flush
pub const fn get_frame(&mut self) -> Frame<'_> {
let count = self.frame_count;
Frame {
cursor_position: None,
viewport_area: self.viewport_area,
buffer: self.current_buffer_mut(),
count,
}
}
/// Gets the current buffer as a mutable reference.
///
/// This is the buffer that the next [`Frame`] will render into (see [`Terminal::get_frame`]).
/// Most applications should render inside [`Terminal::draw`] and access the buffer via
/// [`Frame::buffer_mut`] instead.
pub const fn current_buffer_mut(&mut self) -> &mut Buffer {
&mut self.buffers[self.current]
}
/// Writes the current buffer to the backend using a diff against the previous buffer.
///
/// This is one of the building blocks used by [`Terminal::draw`] / [`Terminal::try_draw`]. It
/// does not swap buffers or flush the backend; see [`Terminal::swap_buffers`] and
/// [`Backend::flush`].
///
/// Implementation note: when there are updates, Ratatui records the position of the last
/// updated cell as the "last known cursor position". Inline viewports use this to preserve the
/// cursor's relative position within the viewport across resizes.
///
/// [`Backend::flush`]: crate::backend::Backend::flush
pub fn flush(&mut self) -> Result<(), B::Error> {
let previous_buffer = &self.buffers[1 - self.current];
let current_buffer = &self.buffers[self.current];
let updates = previous_buffer.diff(current_buffer);
if let Some((col, row, _)) = updates.last() {
self.last_known_cursor_pos = Position { x: *col, y: *row };
}
self.backend.draw(updates.into_iter())
}
/// Clears the inactive buffer and swaps it with the current buffer.
///
/// This is part of the standard rendering flow (see [`Terminal::try_draw`]). If you render
/// manually using [`Terminal::get_frame`] and [`Terminal::flush`], call this afterward so the
/// next flush can compute diffs against the correct "previous" buffer.
pub fn swap_buffers(&mut self) {
self.buffers[1 - self.current].reset();
self.current = 1 - self.current;
}
/// Clear the terminal and force a full redraw on the next draw call.
///
/// What gets cleared depends on the active [`Viewport`]:
///
/// - [`Viewport::Fullscreen`]: clears the entire terminal.
/// - [`Viewport::Fixed`]: clears only the viewport region.
/// - [`Viewport::Inline`]: clears after the viewport's origin, leaving any content above the
/// viewport untouched.
///
/// Current behavior: for [`Viewport::Inline`], clearing runs from the viewport origin through
/// the end of the visible display area, not just the viewport's rectangle. This is an
/// implementation detail rather than a contract; do not rely on it.
///
/// This preserves the cursor position.
///
/// This also resets the "previous" buffer so the next [`Terminal::flush`] redraws the full
/// viewport. [`Terminal::resize`] calls this internally.
///
/// Implementation note: this uses [`ClearType::AfterCursor`] starting at the viewport origin.
pub fn clear(&mut self) -> Result<(), B::Error> {
let original_cursor = self.backend.get_cursor_position()?;
match self.viewport {
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
Viewport::Inline(_) => {
self.backend
.set_cursor_position(self.viewport_area.as_position())?;
self.backend.clear_region(ClearType::AfterCursor)?;
}
Viewport::Fixed(_) => {
let area = self.viewport_area;
self.clear_fixed_viewport(area)?;
}
}
self.backend.set_cursor_position(original_cursor)?;
// Reset the back buffer to make sure the next update will redraw everything.
self.buffers[1 - self.current].reset();
Ok(())
}
/// Clears a fixed viewport using terminal clear commands when possible.
///
/// Terminal clear commands can be faster than per-cell updates.
fn clear_fixed_viewport(&mut self, area: Rect) -> Result<(), B::Error> {
if area.is_empty() {
return Ok(());
}
let size = self.backend.size()?;
let is_full_width = area.x == 0 && area.width == size.width;
let ends_at_bottom = area.bottom() == size.height;
if is_full_width && ends_at_bottom {
self.backend.set_cursor_position(area.as_position())?;
self.backend.clear_region(ClearType::AfterCursor)?;
} else if is_full_width {
self.clear_full_width_rows(area)?;
} else {
self.clear_region_cells(area)?;
}
Ok(())
}
/// Clears full-width rows using line clear commands.
///
/// This avoids per-cell writes when the viewport spans the full width.
fn clear_full_width_rows(&mut self, area: Rect) -> Result<(), B::Error> {
for y in area.top()..area.bottom() {
self.backend.set_cursor_position(Position { x: 0, y })?;
self.backend.clear_region(ClearType::CurrentLine)?;
}
Ok(())
}
/// Clears a non-full-width region by writing empty cells directly.
///
/// This is used when line-based clears would affect cells outside the viewport.
fn clear_region_cells(&mut self, area: Rect) -> Result<(), B::Error> {
let clear_cell = Cell::default();
let updates = area.positions().map(|pos| (pos.x, pos.y, &clear_cell));
self.backend.draw(updates)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::backend::{Backend, TestBackend};
use crate::buffer::{Buffer, Cell};
use crate::layout::{Position, Rect};
use crate::terminal::{Terminal, TerminalOptions, Viewport};
#[test]
fn get_frame_uses_current_viewport_and_frame_count() {
let backend = TestBackend::new(5, 3);
let mut terminal = Terminal::new(backend).unwrap();
let frame = terminal.get_frame();
assert_eq!(frame.count, 0);
assert_eq!(frame.area().width, 5);
assert_eq!(frame.area().height, 3);
assert_eq!(frame.buffer.area, frame.area());
}
#[test]
fn flush_writes_updates_and_tracks_last_updated_cell() {
let backend = TestBackend::new(3, 2);
let mut terminal = Terminal::new(backend).unwrap();
{
let frame = terminal.get_frame();
frame.buffer[(1, 0)].set_symbol("x");
}
terminal.flush().unwrap();
terminal.backend().assert_buffer_lines([" x ", " "]);
assert_eq!(terminal.last_known_cursor_pos, Position { x: 1, y: 0 });
}
#[test]
fn flush_with_no_updates_does_not_change_last_known_cursor_pos() {
let backend = TestBackend::new(3, 2);
let mut terminal = Terminal::new(backend).unwrap();
terminal.set_cursor_position((2, 1)).unwrap();
terminal.flush().unwrap();
assert_eq!(terminal.last_known_cursor_pos, Position { x: 2, y: 1 });
}
#[test]
fn swap_buffers_resets_new_current_buffer() {
let backend = TestBackend::new(3, 2);
let mut terminal = Terminal::new(backend).unwrap();
terminal.buffers[1][(0, 0)].set_symbol("x");
terminal.swap_buffers();
assert_eq!(terminal.current, 1);
assert_eq!(
terminal.buffers[terminal.current],
Buffer::empty(terminal.viewport_area)
);
}
#[test]
fn clear_fullscreen_clears_backend_and_resets_back_buffer() {
let backend = TestBackend::new(3, 2);
let mut terminal = Terminal::new(backend).unwrap();
{
let frame = terminal.get_frame();
frame.buffer[(0, 0)] = Cell::new("x");
}
terminal.flush().unwrap();
terminal.backend().assert_buffer_lines(["x ", " "]);
terminal.buffers[1][(2, 1)] = Cell::new("y");
terminal.clear().unwrap();
terminal.backend().assert_buffer_lines([" ", " "]);
assert_eq!(
terminal.buffers[1 - terminal.current],
Buffer::empty(terminal.viewport_area)
);
}
#[test]
fn clear_inline_clears_after_viewport_origin_and_resets_back_buffer() {
// Inline clear is implemented as:
// 1) move the backend cursor to the viewport origin
// 2) call ClearType::AfterCursor once
let mut backend = TestBackend::with_lines([
"before 1 ",
"before 2 ",
"viewport 1",
"viewport 2",
"after 1 ",
"after 2 ",
]);
backend
.set_cursor_position(Position { x: 2, y: 2 })
.unwrap();
let options = TerminalOptions {
viewport: Viewport::Inline(2),
};
let mut terminal = Terminal::with_options(backend, options).unwrap();
terminal
.backend_mut()
.set_cursor_position(Position { x: 2, y: 2 })
.unwrap();
terminal.buffers[1][(2, 2)] = Cell::new("x");
terminal.clear().unwrap();
// Inline viewport is anchored to the cursor row (y = 2) with height 2. Clear runs from
// the viewport origin through the end of the display, including the rows after it.
terminal.backend().assert_buffer_lines([
"before 1 ",
"before 2 ",
" ",
" ",
" ",
" ",
]);
assert_eq!(
terminal.buffers[1 - terminal.current],
Buffer::empty(terminal.viewport_area)
);
assert_eq!(
terminal.backend().cursor_position(),
Position { x: 2, y: 2 }
);
}
#[test]
fn clear_fixed_clears_viewport_rows_and_resets_back_buffer() {
// For full-width fixed viewports that reach the terminal bottom, clear uses
// ClearType::AfterCursor starting at the viewport origin.
let mut backend = TestBackend::with_lines(["before 1 ", "viewport 1", "viewport 2"]);
backend.set_cursor_position((2, 0)).unwrap();
let options = TerminalOptions {
viewport: Viewport::Fixed(Rect::new(0, 1, 10, 2)),
};
let mut terminal = Terminal::with_options(backend, options).unwrap();
terminal.clear().unwrap();
terminal
.backend()
.assert_buffer_lines(["before 1 ", " ", " "]);
assert_eq!(
terminal.buffers[1 - terminal.current],
Buffer::empty(terminal.viewport_area)
);
assert_eq!(
terminal.backend().cursor_position(),
Position { x: 2, y: 0 }
);
}
#[test]
fn clear_fixed_full_width_not_at_bottom() {
let mut backend =
TestBackend::with_lines(["before 1 ", "viewport 1", "viewport 2", "after 1 "]);
backend.set_cursor_position((1, 0)).unwrap();
let options = TerminalOptions {
viewport: Viewport::Fixed(Rect::new(0, 1, 10, 2)),
};
let mut terminal = Terminal::with_options(backend, options).unwrap();
terminal.clear().unwrap();
terminal.backend().assert_buffer_lines([
"before 1 ",
" ",
" ",
"after 1 ",
]);
assert_eq!(
terminal.backend().cursor_position(),
Position { x: 1, y: 0 }
);
}
#[test]
fn clear_fixed_respects_non_full_width_viewport() {
let mut backend =
TestBackend::with_lines(["before 1 ", "viewport 1", "viewport 2", "after 1 "]);
backend.set_cursor_position((3, 0)).unwrap();
let options = TerminalOptions {
viewport: Viewport::Fixed(Rect::new(1, 1, 3, 2)),
};
let mut terminal = Terminal::with_options(backend, options).unwrap();
terminal.clear().unwrap();
terminal.backend().assert_buffer_lines([
"before 1 ",
"v port 1",
"v port 2",
"after 1 ",
]);
assert_eq!(
terminal.backend().cursor_position(),
Position { x: 3, y: 0 }
);
}
}

View File

@@ -0,0 +1,151 @@
use crate::backend::Backend;
use crate::layout::Position;
use crate::terminal::Terminal;
impl<B: Backend> Terminal<B> {
/// Hides the cursor.
///
/// When using [`Terminal::draw`], prefer controlling the cursor with
/// [`Frame::set_cursor_position`]. Mixing the APIs can lead to surprising results.
///
/// [`Frame::set_cursor_position`]: crate::terminal::Frame::set_cursor_position
/// [`Terminal::draw`]: crate::terminal::Terminal::draw
pub fn hide_cursor(&mut self) -> Result<(), B::Error> {
self.backend.hide_cursor()?;
self.hidden_cursor = true;
Ok(())
}
/// Shows the cursor.
///
/// When using [`Terminal::draw`], prefer controlling the cursor with
/// [`Frame::set_cursor_position`]. Mixing the APIs can lead to surprising results.
///
/// [`Frame::set_cursor_position`]: crate::terminal::Frame::set_cursor_position
/// [`Terminal::draw`]: crate::terminal::Terminal::draw
pub fn show_cursor(&mut self) -> Result<(), B::Error> {
self.backend.show_cursor()?;
self.hidden_cursor = false;
Ok(())
}
/// Gets the current cursor position.
///
/// This is the position of the cursor after the last draw call and is returned as a tuple of
/// `(x, y)` coordinates.
#[deprecated = "use `get_cursor_position()` instead which returns `Result<Position>`"]
pub fn get_cursor(&mut self) -> Result<(u16, u16), B::Error> {
let Position { x, y } = self.get_cursor_position()?;
Ok((x, y))
}
/// Sets the cursor position.
#[deprecated = "use `set_cursor_position((x, y))` instead which takes `impl Into<Position>`"]
pub fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), B::Error> {
self.set_cursor_position(Position { x, y })
}
/// Gets the current cursor position.
///
/// This queries the backend for the current cursor position.
///
/// When using [`Terminal::draw`], prefer controlling the cursor with
/// [`Frame::set_cursor_position`]. For direct control, see [`Terminal::set_cursor_position`].
///
/// [`Frame::set_cursor_position`]: crate::terminal::Frame::set_cursor_position
/// [`Terminal::draw`]: crate::terminal::Terminal::draw
pub fn get_cursor_position(&mut self) -> Result<Position, B::Error> {
self.backend.get_cursor_position()
}
/// Sets the cursor position.
///
/// This updates the backend cursor and Ratatui's internal cursor tracking. Inline viewports
/// use that tracking when recomputing the viewport on resize.
///
/// When using [`Terminal::draw`], consider using [`Frame::set_cursor_position`] instead so the
/// cursor is updated as part of the normal rendering flow.
///
/// [`Frame::set_cursor_position`]: crate::terminal::Frame::set_cursor_position
/// [`Terminal::draw`]: crate::terminal::Terminal::draw
pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> Result<(), B::Error> {
let position = position.into();
self.backend.set_cursor_position(position)?;
self.last_known_cursor_pos = position;
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::backend::{Backend, TestBackend};
use crate::layout::Position;
use crate::terminal::Terminal;
#[test]
fn hide_cursor_updates_terminal_state() {
let backend = TestBackend::new(10, 5);
let mut terminal = Terminal::new(backend).unwrap();
terminal.hide_cursor().unwrap();
assert!(terminal.hidden_cursor);
assert!(!terminal.backend().cursor_visible());
}
#[test]
fn show_cursor_updates_terminal_state() {
let backend = TestBackend::new(10, 5);
let mut terminal = Terminal::new(backend).unwrap();
terminal.hide_cursor().unwrap();
terminal.show_cursor().unwrap();
assert!(!terminal.hidden_cursor);
assert!(terminal.backend().cursor_visible());
}
#[test]
fn set_cursor_position_updates_backend_and_tracking() {
let backend = TestBackend::new(10, 5);
let mut terminal = Terminal::new(backend).unwrap();
terminal.set_cursor_position((3, 4)).unwrap();
assert_eq!(terminal.last_known_cursor_pos, Position { x: 3, y: 4 });
terminal
.backend_mut()
.assert_cursor_position(Position { x: 3, y: 4 });
}
#[test]
fn get_cursor_position_queries_backend() {
let backend = TestBackend::new(10, 5);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.backend_mut()
.set_cursor_position(Position { x: 7, y: 2 })
.unwrap();
assert_eq!(
terminal.get_cursor_position().unwrap(),
Position { x: 7, y: 2 }
);
}
#[test]
#[allow(deprecated)]
fn deprecated_cursor_wrappers_delegate_to_position_apis() {
let backend = TestBackend::new(10, 5);
let mut terminal = Terminal::new(backend).unwrap();
terminal.set_cursor(4, 1).unwrap();
assert_eq!(terminal.get_cursor().unwrap(), (4, 1));
assert_eq!(terminal.last_known_cursor_pos, Position { x: 4, y: 1 });
terminal
.backend_mut()
.assert_cursor_position(Position { x: 4, y: 1 });
}
}

View File

@@ -0,0 +1,236 @@
use crate::backend::Backend;
use crate::buffer::Buffer;
use crate::layout::Position;
use crate::terminal::inline::compute_inline_size;
use crate::terminal::{Terminal, TerminalOptions, Viewport};
impl<B: Backend> Terminal<B> {
/// Creates a new [`Terminal`] with the given [`Backend`] with a full screen viewport.
///
/// This is a convenience for [`Terminal::with_options`] with [`Viewport::Fullscreen`].
///
/// After creating a terminal, call [`Terminal::draw`] (or [`Terminal::try_draw`]) in a loop to
/// render your UI.
///
/// Note that unlike [`ratatui::init`], this does not install a panic hook, so it is
/// recommended to do that manually when using this function, otherwise any panic messages will
/// be printed to the alternate screen and the terminal may be left in an unusable state.
///
/// See [how to set up panic hooks](https://ratatui.rs/recipes/apps/panic-hooks/) and
/// [`better-panic` example](https://ratatui.rs/recipes/apps/better-panic/) for more
/// information.
///
/// # Example
///
/// ```rust,no_run
/// # #![allow(unexpected_cfgs)]
/// # #[cfg(feature = "crossterm")]
/// # {
/// use std::io::stdout;
///
/// use ratatui::Terminal;
/// use ratatui::backend::CrosstermBackend;
///
/// let backend = CrosstermBackend::new(stdout());
/// let _terminal = Terminal::new(backend)?;
///
/// // Optionally set up a panic hook to restore the terminal on panic.
/// let old_hook = std::panic::take_hook();
/// std::panic::set_hook(Box::new(move |info| {
/// ratatui::restore();
/// old_hook(info);
/// }));
/// # }
/// # #[cfg(not(feature = "crossterm"))]
/// # {
/// # use ratatui_core::{backend::TestBackend, terminal::Terminal};
/// # let backend = TestBackend::new(10, 10);
/// # let _terminal = Terminal::new(backend)?;
/// # }
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// [`ratatui::init`]: https://docs.rs/ratatui/latest/ratatui/fn.init.html
pub fn new(backend: B) -> Result<Self, B::Error> {
Self::with_options(
backend,
TerminalOptions {
viewport: Viewport::Fullscreen,
},
)
}
/// Creates a new [`Terminal`] with the given [`Backend`] and [`TerminalOptions`].
///
/// The viewport determines what area is exposed to widgets via [`Frame::area`]. See
/// [`Viewport`] for an overview of the available modes.
///
/// [`Frame::area`]: crate::terminal::Frame::area
///
/// After creating a terminal, call [`Terminal::draw`] (or [`Terminal::try_draw`]) in a loop to
/// render your UI.
///
/// Resize behavior depends on the selected viewport:
///
/// - [`Viewport::Fullscreen`] and [`Viewport::Inline`] are automatically resized during
/// [`Terminal::draw`] (via [`Terminal::autoresize`]).
/// - [`Viewport::Fixed`] is not automatically resized; call [`Terminal::resize`] if the region
/// should change.
///
/// # Example
///
/// ```rust,no_run
/// # #![allow(unexpected_cfgs)]
/// # #[cfg(feature = "crossterm")]
/// # {
/// use std::io::stdout;
///
/// use ratatui::backend::CrosstermBackend;
/// use ratatui::layout::Rect;
/// use ratatui::{Terminal, TerminalOptions, Viewport};
///
/// let backend = CrosstermBackend::new(stdout());
/// let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
/// let _terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
/// # }
/// # #[cfg(not(feature = "crossterm"))]
/// # {
/// # use ratatui_core::{
/// # backend::TestBackend,
/// # layout::Rect,
/// # terminal::{Terminal, TerminalOptions, Viewport},
/// # };
/// # let backend = TestBackend::new(10, 10);
/// # let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
/// # let _terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
/// # }
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// When the viewport is [`Viewport::Inline`], Ratatui anchors the viewport to the current
/// cursor row at initialization time (always starting at column 0). Ratatui may scroll the
/// terminal to make enough room for the requested height so the viewport stays fully visible.
pub fn with_options(mut backend: B, options: TerminalOptions) -> Result<Self, B::Error> {
let area = match options.viewport {
Viewport::Fullscreen | Viewport::Inline(_) => backend.size()?.into(),
Viewport::Fixed(area) => area,
};
let (viewport_area, cursor_pos) = match options.viewport {
Viewport::Fullscreen => (area, Position::ORIGIN),
Viewport::Inline(height) => {
compute_inline_size(&mut backend, height, area.as_size(), 0)?
}
Viewport::Fixed(area) => (area, area.as_position()),
};
Ok(Self {
backend,
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
current: 0,
hidden_cursor: false,
viewport: options.viewport,
viewport_area,
last_known_area: area,
last_known_cursor_pos: cursor_pos,
frame_count: 0,
})
}
}
#[cfg(test)]
mod tests {
use crate::backend::{Backend, TestBackend};
use crate::layout::{Position, Rect};
use crate::terminal::{Terminal, TerminalOptions, Viewport};
#[test]
fn new_fullscreen_initializes_state() {
let backend = TestBackend::new(10, 5);
let terminal = Terminal::new(backend).unwrap();
assert_eq!(terminal.viewport, Viewport::Fullscreen);
assert_eq!(terminal.viewport_area, Rect::new(0, 0, 10, 5));
assert_eq!(terminal.last_known_area, Rect::new(0, 0, 10, 5));
assert_eq!(terminal.last_known_cursor_pos, Position::ORIGIN);
assert_eq!(terminal.current, 0);
assert!(!terminal.hidden_cursor);
assert_eq!(terminal.frame_count, 0);
assert_eq!(terminal.buffers[0].area, terminal.viewport_area);
assert_eq!(terminal.buffers[1].area, terminal.viewport_area);
}
#[test]
fn with_options_fixed_uses_fixed_area() {
let backend = TestBackend::new(10, 10);
let viewport = Viewport::Fixed(Rect::new(2, 3, 5, 4));
let terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: viewport.clone(),
},
)
.unwrap();
assert_eq!(terminal.viewport, viewport);
assert_eq!(terminal.viewport_area, Rect::new(2, 3, 5, 4));
assert_eq!(terminal.last_known_area, Rect::new(2, 3, 5, 4));
assert_eq!(terminal.last_known_cursor_pos, Position { x: 2, y: 3 });
assert_eq!(terminal.buffers[0].area, terminal.viewport_area);
assert_eq!(terminal.buffers[1].area, terminal.viewport_area);
}
#[test]
fn with_options_inline_anchors_to_cursor_when_space_available() {
let mut backend = TestBackend::new(10, 10);
backend
.set_cursor_position(Position { x: 0, y: 3 })
.unwrap();
let terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(4),
},
)
.unwrap();
assert_eq!(terminal.viewport_area, Rect::new(0, 3, 10, 4));
assert_eq!(terminal.last_known_cursor_pos, Position { x: 0, y: 3 });
}
#[test]
fn with_options_inline_shifts_up_when_near_bottom() {
let mut backend = TestBackend::new(10, 10);
backend
.set_cursor_position(Position { x: 0, y: 8 })
.unwrap();
let terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(4),
},
)
.unwrap();
assert_eq!(terminal.viewport_area, Rect::new(0, 6, 10, 4));
assert_eq!(terminal.last_known_cursor_pos, Position { x: 0, y: 8 });
}
#[test]
fn with_options_inline_clamps_height_to_terminal() {
let mut backend = TestBackend::new(10, 3);
backend
.set_cursor_position(Position { x: 0, y: 0 })
.unwrap();
let terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(10),
},
)
.unwrap();
assert_eq!(terminal.viewport_area, Rect::new(0, 0, 10, 3));
}
}

View File

@@ -0,0 +1,927 @@
use crate::backend::Backend;
use crate::buffer::{Buffer, Cell};
use crate::layout::{Position, Rect, Size};
use crate::terminal::{Terminal, Viewport};
impl<B: Backend> Terminal<B> {
/// Insert some content before the current inline viewport. This has no effect when the
/// viewport is not inline.
///
/// This is intended for inline UIs that want to print output (e.g. logs or status messages)
/// above the UI without breaking it. See [`Viewport::Inline`] for how inline viewports are
/// anchored.
///
/// The `draw_fn` closure will be called to draw into a writable `Buffer` that is `height`
/// lines tall. The content of that `Buffer` will then be inserted before the viewport.
///
/// When Ratatui is built with the `scrolling-regions` feature, this can be done without
/// clearing and redrawing the viewport. Without `scrolling-regions`, Ratatui falls back to a
/// more portable approach and clears the viewport so the next [`Terminal::draw`] repaints it.
///
/// If the viewport isn't yet at the bottom of the screen, inserted lines will push it towards
/// the bottom. Once the viewport is at the bottom of the screen, inserted lines will scroll
/// the area of the screen above the viewport upwards.
///
/// Before:
/// ```text
/// +---------------------+
/// | pre-existing line 1 |
/// | pre-existing line 2 |
/// +---------------------+
/// | viewport |
/// +---------------------+
/// | |
/// | |
/// +---------------------+
/// ```
///
/// After inserting 2 lines:
/// ```text
/// +---------------------+
/// | pre-existing line 1 |
/// | pre-existing line 2 |
/// | inserted line 1 |
/// | inserted line 2 |
/// +---------------------+
/// | viewport |
/// +---------------------+
/// +---------------------+
/// ```
///
/// After inserting 2 more lines:
/// ```text
/// +---------------------+
/// | pre-existing line 2 |
/// | inserted line 1 |
/// | inserted line 2 |
/// | inserted line 3 |
/// | inserted line 4 |
/// +---------------------+
/// | viewport |
/// +---------------------+
/// ```
///
/// If more lines are inserted than there is space on the screen, then the top lines will go
/// directly into the terminal's scrollback buffer. At the limit, if the viewport takes up the
/// whole screen, all lines will be inserted directly into the scrollback buffer.
///
/// # Examples
///
/// ## Insert a single line before the current viewport
///
/// ```rust,no_run
/// # mod ratatui {
/// # pub use ratatui_core::backend;
/// # pub use ratatui_core::layout;
/// # pub use ratatui_core::style;
/// # pub use ratatui_core::terminal::{Terminal, TerminalOptions, Viewport};
/// # pub use ratatui_core::text;
/// # pub use ratatui_core::widgets;
/// # }
/// use ratatui::backend::{Backend, TestBackend};
/// use ratatui::layout::Position;
/// use ratatui::style::{Color, Style};
/// use ratatui::text::{Line, Span};
/// use ratatui::widgets::Widget;
/// use ratatui::{Terminal, TerminalOptions, Viewport};
///
/// let mut backend = TestBackend::new(10, 10);
/// // Simulate existing output above the inline UI.
/// backend.set_cursor_position(Position::new(0, 3))?;
/// let mut terminal = Terminal::with_options(
/// backend,
/// TerminalOptions {
/// viewport: Viewport::Inline(4),
/// },
/// )?;
///
/// terminal.insert_before(1, |buf| {
/// Line::from(vec![
/// Span::raw("This line will be added "),
/// Span::styled("before", Style::default().fg(Color::Blue)),
/// Span::raw(" the current viewport"),
/// ])
/// .render(buf.area, buf);
/// })?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> Result<(), B::Error>
where
F: FnOnce(&mut Buffer),
{
match self.viewport {
#[cfg(feature = "scrolling-regions")]
Viewport::Inline(_) => self.insert_before_scrolling_regions(height, draw_fn),
#[cfg(not(feature = "scrolling-regions"))]
Viewport::Inline(_) => self.insert_before_no_scrolling_regions(height, draw_fn),
_ => Ok(()),
}
}
/// Implement `Self::insert_before` using standard backend capabilities.
///
/// This is the fallback implementation when the `scrolling-regions` feature is disabled. It
/// renders the inserted lines into a temporary [`Buffer`], then draws them directly to the
/// backend in chunks, scrolling the terminal as needed.
///
/// See [`Terminal::insert_before`] for the public API contract.
#[cfg(not(feature = "scrolling-regions"))]
fn insert_before_no_scrolling_regions(
&mut self,
height: u16,
draw_fn: impl FnOnce(&mut Buffer),
) -> Result<(), B::Error> {
let area = Rect {
x: 0,
y: 0,
width: self.viewport_area.width,
height,
};
let mut buffer = Buffer::empty(area);
draw_fn(&mut buffer);
let mut buffer = buffer.content.as_slice();
// Use i32 variables so we don't have worry about overflowed u16s when adding, or about
// negative results when subtracting.
let mut drawn_height: i32 = self.viewport_area.top().into();
let mut buffer_height: i32 = height.into();
let viewport_height: i32 = self.viewport_area.height.into();
let screen_height: i32 = self.last_known_area.height.into();
// The algorithm here is to loop, drawing large chunks of text (up to a screen-full at a
// time), until the remainder of the buffer plus the viewport fits on the screen. We choose
// this loop condition because it guarantees that we can write the remainder of the buffer
// with just one call to Self::draw_lines().
while buffer_height + viewport_height > screen_height {
// We will draw as much of the buffer as possible on this iteration in order to make
// forward progress. So we have:
//
// to_draw = min(buffer_height, screen_height)
//
// We may need to scroll the screen up to make room to draw. We choose the minimal
// possible scroll amount so we don't end up with the viewport sitting in the middle of
// the screen when this function is done. The amount to scroll by is:
//
// scroll_up = max(0, drawn_height + to_draw - screen_height)
//
// We want `scroll_up` to be enough so that, after drawing, we have used the whole
// screen (drawn_height - scroll_up + to_draw = screen_height). However, there might
// already be enough room on the screen to draw without scrolling (drawn_height +
// to_draw <= screen_height). In this case, we just don't scroll at all.
let to_draw = buffer_height.min(screen_height);
let scroll_up = 0.max(drawn_height + to_draw - screen_height);
self.scroll_up(scroll_up as u16)?;
buffer = self.draw_lines((drawn_height - scroll_up) as u16, to_draw as u16, buffer)?;
drawn_height += to_draw - scroll_up;
buffer_height -= to_draw;
}
// There is now enough room on the screen for the remaining buffer plus the viewport,
// though we may still need to scroll up some of the existing text first. It's possible
// that by this point we've drained the buffer, but we may still need to scroll up to make
// room for the viewport.
//
// We want to scroll up the exact amount that will leave us completely filling the screen.
// However, it's possible that the viewport didn't start on the bottom of the screen and
// the added lines weren't enough to push it all the way to the bottom. We deal with this
// case by just ensuring that our scroll amount is non-negative.
//
// We want:
// screen_height = drawn_height - scroll_up + buffer_height + viewport_height
// Or, equivalently:
// scroll_up = drawn_height + buffer_height + viewport_height - screen_height
let scroll_up = 0.max(drawn_height + buffer_height + viewport_height - screen_height);
self.scroll_up(scroll_up as u16)?;
self.draw_lines(
(drawn_height - scroll_up) as u16,
buffer_height as u16,
buffer,
)?;
drawn_height += buffer_height - scroll_up;
self.set_viewport_area(Rect {
y: drawn_height as u16,
..self.viewport_area
});
// Clear the viewport off the screen. We didn't clear earlier for two reasons. First, it
// wasn't necessary because the buffer we drew out of isn't sparse, so it overwrote
// whatever was on the screen. Second, there is a weird bug with tmux where a full screen
// clear plus immediate scrolling causes some garbage to go into the scrollback.
self.clear()?;
Ok(())
}
/// Implement `Self::insert_before` using scrolling regions.
///
/// If a terminal supports scrolling regions, it means that we can define a subset of rows of
/// the screen, and then tell the terminal to scroll up or down just within that region. The
/// rows outside of the region are not affected.
///
/// This function utilizes this feature to avoid having to redraw the viewport. This is done
/// either by splitting the screen at the top of the viewport, and then creating a gap by
/// either scrolling the viewport down, or scrolling the area above it up. The lines to insert
/// are then drawn into the gap created.
#[cfg(feature = "scrolling-regions")]
fn insert_before_scrolling_regions(
&mut self,
mut height: u16,
draw_fn: impl FnOnce(&mut Buffer),
) -> Result<(), B::Error> {
let area = Rect {
x: 0,
y: 0,
width: self.viewport_area.width,
height,
};
let mut buffer = Buffer::empty(area);
draw_fn(&mut buffer);
let mut buffer = buffer.content.as_slice();
// Handle the special case where the viewport takes up the whole screen.
if self.viewport_area.height == self.last_known_area.height {
// "Borrow" the top line of the viewport. Draw over it, then immediately scroll it into
// scrollback. Do this repeatedly until the whole buffer has been put into scrollback.
let mut first = true;
while !buffer.is_empty() {
buffer = if first {
self.draw_lines(0, 1, buffer)?
} else {
self.draw_lines_over_cleared(0, 1, buffer)?
};
first = false;
self.backend.scroll_region_up(0..1, 1)?;
}
// Redraw the top line of the viewport.
let width = self.viewport_area.width as usize;
let top_line = self.buffers[1 - self.current].content[0..width].to_vec();
self.draw_lines_over_cleared(0, 1, &top_line)?;
return Ok(());
}
// Handle the case where the viewport isn't yet at the bottom of the screen.
{
let viewport_top = self.viewport_area.top();
let viewport_bottom = self.viewport_area.bottom();
let screen_bottom = self.last_known_area.bottom();
if viewport_bottom < screen_bottom {
let to_draw = height.min(screen_bottom - viewport_bottom);
self.backend
.scroll_region_down(viewport_top..viewport_bottom + to_draw, to_draw)?;
buffer = self.draw_lines_over_cleared(viewport_top, to_draw, buffer)?;
self.set_viewport_area(Rect {
y: viewport_top + to_draw,
..self.viewport_area
});
height -= to_draw;
}
}
let viewport_top = self.viewport_area.top();
while height > 0 {
let to_draw = height.min(viewport_top);
self.backend.scroll_region_up(0..viewport_top, to_draw)?;
buffer = self.draw_lines_over_cleared(viewport_top - to_draw, to_draw, buffer)?;
height -= to_draw;
}
Ok(())
}
/// Draw lines at the given vertical offset. The slice of cells must contain enough cells
/// for the requested lines. A slice of the unused cells are returned.
///
/// This is a small internal helper used by [`Terminal::insert_before`]. It writes cells
/// directly to the backend in terminal coordinates (not viewport coordinates).
fn draw_lines<'a>(
&mut self,
y_offset: u16,
lines_to_draw: u16,
cells: &'a [Cell],
) -> Result<&'a [Cell], B::Error> {
let width: usize = self.last_known_area.width.into();
let (to_draw, remainder) = cells.split_at(width * lines_to_draw as usize);
if lines_to_draw > 0 {
let iter = to_draw
.iter()
.enumerate()
.map(|(i, c)| ((i % width) as u16, y_offset + (i / width) as u16, c));
self.backend.draw(iter)?;
self.backend.flush()?;
}
Ok(remainder)
}
/// Draw lines at the given vertical offset, assuming that the lines they are replacing on the
/// screen are cleared. The slice of cells must contain enough cells for the requested lines. A
/// slice of the unused cells are returned.
///
/// This is used by the `scrolling-regions` implementation of [`Terminal::insert_before`] to
/// avoid relying on a full-screen clear while updating only part of the terminal.
#[cfg(feature = "scrolling-regions")]
fn draw_lines_over_cleared<'a>(
&mut self,
y_offset: u16,
lines_to_draw: u16,
cells: &'a [Cell],
) -> Result<&'a [Cell], B::Error> {
let width: usize = self.last_known_area.width.into();
let (to_draw, remainder) = cells.split_at(width * lines_to_draw as usize);
if lines_to_draw > 0 {
let area = Rect::new(0, y_offset, width as u16, y_offset + lines_to_draw);
let old = Buffer::empty(area);
let new = Buffer {
area,
content: to_draw.to_vec(),
};
self.backend.draw(old.diff(&new).into_iter())?;
self.backend.flush()?;
}
Ok(remainder)
}
/// Scroll the whole screen up by the given number of lines.
///
/// This is used by [`Terminal::insert_before`] when the `scrolling-regions` feature is
/// disabled.
/// It scrolls by moving the cursor to the last row and calling [`Backend::append_lines`].
#[cfg(not(feature = "scrolling-regions"))]
fn scroll_up(&mut self, lines_to_scroll: u16) -> Result<(), B::Error> {
if lines_to_scroll > 0 {
self.set_cursor_position(Position::new(
0,
self.last_known_area.height.saturating_sub(1),
))?;
self.backend.append_lines(lines_to_scroll)?;
}
Ok(())
}
}
/// Compute the on-screen area for an inline viewport.
///
/// This helper is used by [`Terminal::with_options`] (initialization) and [`Terminal::resize`]
/// (after a terminal resize) to translate `Viewport::Inline(height)` into a concrete [`Rect`].
///
/// This returns the computed viewport area and the cursor position observed at the start of the
/// call.
///
/// Inline viewports always start at column 0, span the full terminal width, and are anchored to the
/// backend cursor row at the time of the call. The requested height is clamped to the current
/// terminal height.
///
/// Ratatui reserves vertical space for the requested height by calling [`Backend::append_lines`].
/// If the cursor is close enough to the bottom that appending would run past the last row,
/// terminals scroll; in that case we shift the computed `y` upward by the number of rows scrolled
/// so the viewport remains fully visible.
///
/// `offset_in_previous_viewport` is used by [`Terminal::resize`] to keep the cursor at the same
/// relative row within the viewport across resizes.
///
/// Related viewport code lives in:
///
/// - [`Terminal::with_options`] (selects the viewport and computes the initial area)
/// - [`Terminal::autoresize`] (detects backend size changes during [`Terminal::draw`])
/// - [`Terminal::resize`] (recomputes the viewport and clears before the next draw)
pub(crate) fn compute_inline_size<B: Backend>(
backend: &mut B,
height: u16,
size: Size,
offset_in_previous_viewport: u16,
) -> Result<(Rect, Position), B::Error> {
let pos = backend.get_cursor_position()?;
let mut row = pos.y;
let max_height = size.height.min(height);
let lines_after_cursor = height
.saturating_sub(offset_in_previous_viewport)
.saturating_sub(1);
backend.append_lines(lines_after_cursor)?;
let available_lines = size.height.saturating_sub(row).saturating_sub(1);
let missing_lines = lines_after_cursor.saturating_sub(available_lines);
if missing_lines > 0 {
row = row.saturating_sub(missing_lines);
}
row = row.saturating_sub(offset_in_previous_viewport);
Ok((
Rect {
x: 0,
y: row,
width: size.width,
height: max_height,
},
pos,
))
}
#[cfg(test)]
mod tests {
use crate::backend::{Backend, TestBackend};
use crate::layout::{Position, Rect, Size};
use crate::style::Style;
use crate::terminal::inline::compute_inline_size;
use crate::terminal::{Terminal, TerminalOptions, Viewport};
#[test]
fn compute_inline_size_uses_cursor_offset_when_space_available() {
// Diagram (terminal height = 10, requested viewport height = 4):
//
// Cursor at y=6, previous cursor offset within viewport = 1.
//
// Before (conceptually):
// 0
// 1
// 2
// 3
// 4
// 5 <- viewport top (expected)
// 6 <- cursor row (observed_pos.y)
// 7
// 8
// 9
//
// After: viewport top y = 5 (6 - 1), height = 4 => rows 5..9 (exclusive).
let mut backend = TestBackend::new(10, 10);
backend
.set_cursor_position(Position { x: 0, y: 6 })
.unwrap();
let (area, observed_pos) =
compute_inline_size(&mut backend, 4, Size::new(10, 10), 1).unwrap();
assert_eq!(observed_pos, Position { x: 0, y: 6 });
assert_eq!(area, Rect::new(0, 5, 10, 4));
}
#[test]
fn compute_inline_size_saturates_when_offset_exceeds_cursor_row() {
// Diagram (terminal height = 10, requested viewport height = 4):
//
// Cursor at y=0, previous cursor offset within viewport = 5 (nonsensical but possible if
// callers pass a stale/oversized offset).
//
// We saturate so the computed viewport top cannot go negative:
// top = cursor_y.saturating_sub(offset) = 0.saturating_sub(5) = 0
//
// Expected viewport area:
// y=0..4 (fully pinned to the top)
let mut backend = TestBackend::new(10, 10);
backend
.set_cursor_position(Position { x: 0, y: 0 })
.unwrap();
let (area, _observed_pos) =
compute_inline_size(&mut backend, 4, Size::new(10, 10), 5).unwrap();
assert_eq!(area, Rect::new(0, 0, 10, 4));
}
#[cfg(not(feature = "scrolling-regions"))]
mod no_scrolling_regions {
use super::*;
#[test]
fn insert_before_is_noop_for_non_inline_viewports() {
// Diagram:
//
// Viewport is fullscreen (not inline), so insert_before() is a no-op.
//
// Screen before:
// x..
// ...
//
// Screen after:
// x..
// ...
let mut terminal = Terminal::new(TestBackend::new(3, 2)).unwrap();
{
let frame = terminal.get_frame();
frame.buffer[(0, 0)].set_symbol("x");
}
terminal.flush().unwrap();
let viewport_area = terminal.viewport_area;
terminal
.insert_before(1, |buf| {
buf.set_string(0, 0, "zzz", Style::default());
})
.unwrap();
assert_eq!(terminal.viewport_area, viewport_area);
terminal.backend().assert_buffer_lines(["x ", " "]);
}
#[test]
fn insert_before_pushes_viewport_down_when_space_available() {
// Diagram (screen height = 10, viewport height = 4, cursor row = 3):
//
// Before:
// 0: 0000000000
// 1: 1111111111
// 2: 2222222222
// 3: [viewport top] 3333333333
// 4: 4444444444
// 5: 5555555555
// 6: 6666666666
// 7: 7777777777
// 8: 8888888888
// 9: 9999999999
//
// After inserting 1 line above an inline viewport (no scrolling regions):
// - A line is drawn at the old viewport top (y=3)
// - The viewport moves down by 1 row (new top y=4)
// - The viewport is cleared so it will be redrawn on the next draw()
let mut backend = TestBackend::with_lines([
"0000000000",
"1111111111",
"2222222222",
"3333333333",
"4444444444",
"5555555555",
"6666666666",
"7777777777",
"8888888888",
"9999999999",
]);
backend
.set_cursor_position(Position { x: 0, y: 3 })
.unwrap();
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(4),
},
)
.unwrap();
terminal
.insert_before(1, |buf| {
buf.set_string(0, 0, "INSERTLINE", Style::default());
})
.unwrap();
assert_eq!(terminal.viewport_area, Rect::new(0, 4, 10, 4));
terminal.backend().assert_buffer_lines([
"0000000000",
"1111111111",
"2222222222",
"INSERTLINE",
" ",
" ",
" ",
" ",
" ",
" ",
]);
}
#[test]
fn insert_before_scrolls_when_viewport_is_at_bottom() {
// Diagram (screen height = 10, viewport height = 4, cursor row = 6):
//
// Before:
// 0: 0000000000
// 1: 1111111111
// 2: 2222222222
// 3: 3333333333
// 4: 4444444444
// 5: 5555555555
// 6: [viewport top] 6666666666
// 7: 7777777777
// 8: 8888888888
// 9: 9999999999
//
// After inserting 2 lines:
// - The area above the viewport scrolls up to make room
// - Inserted lines appear immediately above the viewport
// - The viewport is cleared so it will be redrawn on the next draw()
let mut backend = TestBackend::with_lines([
"0000000000",
"1111111111",
"2222222222",
"3333333333",
"4444444444",
"5555555555",
"6666666666",
"7777777777",
"8888888888",
"9999999999",
]);
backend
.set_cursor_position(Position { x: 0, y: 6 })
.unwrap();
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(4),
},
)
.unwrap();
terminal
.insert_before(2, |buf| {
buf.set_string(0, 0, "INSERTED1", Style::default());
buf.set_string(0, 1, "INSERTED2", Style::default());
})
.unwrap();
assert_eq!(terminal.viewport_area, Rect::new(0, 6, 10, 4));
terminal.backend().assert_buffer_lines([
"2222222222",
"3333333333",
"4444444444",
"5555555555",
"INSERTED1 ",
"INSERTED2 ",
" ",
" ",
" ",
" ",
]);
}
#[test]
fn insert_before_then_draw_repaints_cleared_viewport() {
// Diagram (screen height = 10, viewport height = 4, cursor row = 6):
//
// 1) Draw a frame into the inline viewport at the bottom:
// 6..9: AAAAAAAAAA
//
// 2) Insert 2 lines above the viewport:
// - Inserts appear at rows 4..5
// - Viewport is cleared (so it is blank on-screen until the next draw)
//
// 3) Draw again:
// 6..9: BBBBBBBBBB
//
// Expected final screen:
// 4: INSERTED00
// 5: INSERTED01
// 6..9: BBBBBBBBBB
let mut backend = TestBackend::new(10, 10);
backend
.set_cursor_position(Position { x: 0, y: 6 })
.unwrap();
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(4),
},
)
.unwrap();
terminal
.draw(|frame| {
let area = frame.area();
for y in area.top()..area.bottom() {
frame
.buffer
.set_string(area.x, y, "AAAAAAAAAA", Style::default());
}
})
.unwrap();
terminal
.insert_before(2, |buf| {
buf.set_string(0, 0, "INSERTED00", Style::default());
buf.set_string(0, 1, "INSERTED01", Style::default());
})
.unwrap();
terminal
.draw(|frame| {
let area = frame.area();
for y in area.top()..area.bottom() {
frame
.buffer
.set_string(area.x, y, "BBBBBBBBBB", Style::default());
}
})
.unwrap();
terminal.backend().assert_buffer_lines([
" ",
" ",
" ",
" ",
"INSERTED00",
"INSERTED01",
"BBBBBBBBBB",
"BBBBBBBBBB",
"BBBBBBBBBB",
"BBBBBBBBBB",
]);
}
}
#[cfg(feature = "scrolling-regions")]
mod scrolling_regions {
use super::*;
#[test]
fn insert_before_moves_viewport_down_without_clearing() {
// Diagram (screen height = 10, viewport height = 4, cursor row = 3):
//
// With scrolling regions enabled, we can create a gap and draw the inserted line
// without clearing the viewport content.
//
// Before:
// 2: 2222222222
// 3: [viewport top] 3333333333
// 4: 4444444444
//
// After:
// 3: INSERTLINE
// 4: 3333333333 (viewport content preserved)
let mut backend = TestBackend::with_lines([
"0000000000",
"1111111111",
"2222222222",
"3333333333",
"4444444444",
"5555555555",
"6666666666",
"7777777777",
"8888888888",
"9999999999",
]);
backend
.set_cursor_position(Position { x: 0, y: 3 })
.unwrap();
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(4),
},
)
.unwrap();
terminal
.insert_before(1, |buf| {
buf.set_string(0, 0, "INSERTLINE", Style::default());
})
.unwrap();
assert_eq!(terminal.viewport_area, Rect::new(0, 4, 10, 4));
terminal.backend().assert_buffer_lines([
"0000000000",
"1111111111",
"2222222222",
"INSERTLINE",
"3333333333",
"4444444444",
"5555555555",
"6666666666",
"8888888888",
"9999999999",
]);
}
#[test]
fn insert_before_when_viewport_is_at_bottom_preserves_viewport() {
// Diagram (screen height = 10, viewport height = 4, viewport top = 6):
//
// With scrolling regions enabled and the viewport already at the bottom:
// - The region above the viewport (rows 0..6) scrolls up to make room.
// - Inserted lines are drawn into the cleared space immediately above the viewport.
// - The viewport itself is not cleared and stays on-screen.
//
// Before (after drawing V into the viewport):
// 0: 0000000000
// 1: 1111111111
// 2: 2222222222
// 3: 3333333333
// 4: 4444444444
// 5: 5555555555
// 6..9: VVVVVVVVVV
//
// After inserting 2 lines:
// 0..3: previous 2..5
// 4: AAAAAAAAAA
// 5: BBBBBBBBBB
// 6..9: VVVVVVVVVV
//
// The scrolled-off lines are appended to scrollback (previous 0 and 1).
let mut backend = TestBackend::with_lines([
"0000000000",
"1111111111",
"2222222222",
"3333333333",
"4444444444",
"5555555555",
"6666666666",
"7777777777",
"8888888888",
"9999999999",
]);
backend
.set_cursor_position(Position { x: 0, y: 6 })
.unwrap();
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(4),
},
)
.unwrap();
terminal
.draw(|frame| {
let area = frame.area();
for y in area.top()..area.bottom() {
frame
.buffer
.set_string(area.x, y, "VVVVVVVVVV", Style::default());
}
})
.unwrap();
terminal
.insert_before(2, |buf| {
buf.set_string(0, 0, "AAAAAAAAAA", Style::default());
buf.set_string(0, 1, "BBBBBBBBBB", Style::default());
})
.unwrap();
terminal.backend().assert_buffer_lines([
"2222222222",
"3333333333",
"4444444444",
"5555555555",
"AAAAAAAAAA",
"BBBBBBBBBB",
"VVVVVVVVVV",
"VVVVVVVVVV",
"VVVVVVVVVV",
"VVVVVVVVVV",
]);
terminal
.backend()
.assert_scrollback_lines(["0000000000", "1111111111"]);
}
#[test]
fn insert_before_when_viewport_is_fullscreen_appends_to_scrollback() {
// Diagram (screen height = 4, viewport height = 4):
//
// When the viewport takes the whole screen, there is no visible "area above" it.
// The scrolling-regions implementation handles this by repeatedly:
// - drawing one line over the top row
// - immediately scrolling that row into scrollback
//
// The viewport content stays on-screen; inserted lines end up in scrollback.
let mut backend = TestBackend::new(10, 4);
backend
.set_cursor_position(Position { x: 0, y: 0 })
.unwrap();
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(4),
},
)
.unwrap();
terminal
.draw(|frame| {
let area = frame.area();
frame
.buffer
.set_string(area.x, area.y, "VIEWLINE00", Style::default());
frame
.buffer
.set_string(area.x, area.y + 1, "VIEWLINE01", Style::default());
frame
.buffer
.set_string(area.x, area.y + 2, "VIEWLINE02", Style::default());
frame
.buffer
.set_string(area.x, area.y + 3, "VIEWLINE03", Style::default());
})
.unwrap();
terminal
.insert_before(2, |buf| {
buf.set_string(0, 0, "INSERTED00", Style::default());
buf.set_string(0, 1, "INSERTED01", Style::default());
})
.unwrap();
terminal.backend().assert_buffer_lines([
"VIEWLINE00",
"VIEWLINE01",
"VIEWLINE02",
"VIEWLINE03",
]);
terminal
.backend()
.assert_scrollback_lines(["INSERTED00", "INSERTED01"]);
}
}
}

View File

@@ -0,0 +1,737 @@
use crate::backend::Backend;
use crate::terminal::{CompletedFrame, Frame, Terminal};
impl<B: Backend> Terminal<B> {
/// Draws a single frame to the terminal.
///
/// Returns a [`CompletedFrame`] if successful, otherwise a backend error (`B::Error`).
///
/// If the render callback passed to this method can fail, use [`try_draw`] instead.
///
/// Applications should call `draw` or [`try_draw`] in a loop to continuously render the
/// terminal. These methods are the main entry points for drawing to the terminal.
///
/// [`try_draw`]: Terminal::try_draw
///
/// The [`Frame`] passed to the render callback represents the currently configured
/// [`Viewport`] (see [`Frame::area`] and [`Terminal::with_options`]).
///
/// Build layout relative to the [`Rect`] returned by [`Frame::area`] rather than assuming the
/// origin is `(0, 0)`, so the same rendering code works for fixed and inline viewports.
///
/// [`Frame::area`]: crate::terminal::Frame::area
/// [`Rect`]: crate::layout::Rect
/// [`Viewport`]: crate::terminal::Viewport
///
/// This method will:
///
/// - call [`Terminal::autoresize`] if necessary
/// - call the render callback, passing it a [`Frame`] reference to render to
/// - call [`Terminal::flush`] to write changes to the backend
/// - show/hide the cursor based on [`Frame::set_cursor_position`]
/// - call [`Terminal::swap_buffers`] to prepare for the next render pass
/// - call [`Backend::flush`]
/// - return a [`CompletedFrame`] with the current buffer and the area used for rendering
///
/// The [`CompletedFrame`] returned by this method can be useful for debugging or testing
/// purposes, but it is often not used in regular applications.
///
/// The render callback should fully render the entire frame when called, including areas that
/// are unchanged from the previous frame. This is because each frame is compared to the
/// previous frame to determine what has changed, and only the changes are written to the
/// terminal. If the render callback does not fully render the frame, the terminal will not be
/// in a consistent state.
///
/// # Examples
///
/// ```rust,no_run
/// # mod ratatui {
/// # pub use ratatui_core::backend;
/// # pub use ratatui_core::layout;
/// # pub use ratatui_core::terminal::{Frame, Terminal};
/// # }
/// use ratatui::backend::TestBackend;
/// use ratatui::layout::Position;
/// use ratatui::{Frame, Terminal};
///
/// let backend = TestBackend::new(10, 10);
/// let mut terminal = Terminal::new(backend)?;
///
/// // With a closure.
/// terminal.draw(|frame| {
/// let area = frame.area();
/// frame.render_widget("Hello World!", area);
/// frame.set_cursor_position(Position { x: 0, y: 0 });
/// })?;
///
/// // Or with a function.
/// terminal.draw(render)?;
///
/// fn render(frame: &mut Frame<'_>) {
/// frame.render_widget("Hello World!", frame.area());
/// }
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// [`Backend::flush`]: crate::backend::Backend::flush
pub fn draw<F>(&mut self, render_callback: F) -> Result<CompletedFrame<'_>, B::Error>
where
F: FnOnce(&mut Frame),
{
self.try_draw(|frame| {
render_callback(frame);
Ok::<(), B::Error>(())
})
}
/// Tries to draw a single frame to the terminal.
///
/// Returns [`Result::Ok`] containing a [`CompletedFrame`] if successful, otherwise
/// [`Result::Err`] containing the backend error (`B::Error`) that caused the failure.
///
/// This is the equivalent of [`Terminal::draw`] but the render callback is a function or
/// closure that returns a `Result` instead of nothing.
///
/// Applications should call `try_draw` or [`draw`] in a loop to continuously render the
/// terminal. These methods are the main entry points for drawing to the terminal.
///
/// [`draw`]: Terminal::draw
///
/// The [`Frame`] passed to the render callback represents the currently configured
/// [`Viewport`] (see [`Frame::area`] and [`Terminal::with_options`]).
///
/// Build layout relative to the [`Rect`] returned by [`Frame::area`] rather than assuming the
/// origin is `(0, 0)`, so the same rendering code works for fixed and inline viewports.
///
/// [`Frame::area`]: crate::terminal::Frame::area
/// [`Rect`]: crate::layout::Rect
/// [`Viewport`]: crate::terminal::Viewport
///
/// This method will:
///
/// - call [`Terminal::autoresize`] if necessary
/// - call the render callback, passing it a [`Frame`] reference to render to
/// - call [`Terminal::flush`] to write changes to the backend
/// - show/hide the cursor based on [`Frame::set_cursor_position`]
/// - call [`Terminal::swap_buffers`] to prepare for the next render pass
/// - call [`Backend::flush`]
/// - return a [`CompletedFrame`] with the current buffer and the area used for rendering
///
/// The render callback passed to `try_draw` can return any [`Result`] with an error type that
/// can be converted into `B::Error` using the [`Into`] trait. This makes it possible to use the
/// `?` operator to propagate errors that occur during rendering. If the render callback returns
/// an error, the error will be returned from `try_draw` and the terminal will not be updated.
///
/// The [`CompletedFrame`] returned by this method can be useful for debugging or testing
/// purposes, but it is often not used in regular applications.
///
/// The render callback should fully render the entire frame when called, including areas that
/// are unchanged from the previous frame. This is because each frame is compared to the
/// previous frame to determine what has changed, and only the changes are written to the
/// terminal. If the render function does not fully render the frame, the terminal will not be
/// in a consistent state.
///
/// # Examples
///
/// ```rust,no_run
/// # #![allow(unexpected_cfgs)]
/// # #[cfg(feature = "crossterm")]
/// # {
/// use std::io;
///
/// use ratatui::backend::CrosstermBackend;
/// use ratatui::layout::Position;
/// use ratatui::{Frame, Terminal};
///
/// let backend = CrosstermBackend::new(std::io::stdout());
/// let mut terminal = Terminal::new(backend)?;
///
/// // With a closure that returns `Result`.
/// terminal.try_draw(|frame| -> io::Result<()> {
/// let _value: u8 = "42".parse().map_err(io::Error::other)?;
/// let area = frame.area();
/// frame.render_widget("Hello World!", area);
/// frame.set_cursor_position(Position { x: 0, y: 0 });
/// Ok(())
/// })?;
///
/// // Or with a function.
/// terminal.try_draw(render)?;
///
/// fn render(frame: &mut Frame<'_>) -> io::Result<()> {
/// frame.render_widget("Hello World!", frame.area());
/// Ok(())
/// }
/// # }
/// # #[cfg(not(feature = "crossterm"))]
/// # {
/// # use ratatui_core::{backend::TestBackend, terminal::Terminal};
/// # let backend = TestBackend::new(10, 10);
/// # let mut terminal = Terminal::new(backend)?;
/// # terminal
/// # .try_draw(|frame| {
/// # frame.render_widget("Hello World!", frame.area());
/// # Ok::<(), core::convert::Infallible>(())
/// # })
/// # ?;
/// # }
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// [`Backend::flush`]: crate::backend::Backend::flush
pub fn try_draw<F, E>(&mut self, render_callback: F) -> Result<CompletedFrame<'_>, B::Error>
where
F: FnOnce(&mut Frame) -> Result<(), E>,
E: Into<B::Error>,
{
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
// and the terminal (if growing), which may OOB.
self.autoresize()?;
let mut frame = self.get_frame();
render_callback(&mut frame).map_err(Into::into)?;
// We can't change the cursor position right away because we have to flush the frame to
// stdout first. But we also can't keep the frame around, since it holds a &mut to
// Buffer. Thus, we're taking the important data out of the Frame and dropping it.
let cursor_position = frame.cursor_position;
// Apply the buffer diff to the backend (this is the terminal's "flush" step, distinct
// from `Backend::flush` below which flushes the backend's output).
self.flush()?;
match cursor_position {
None => self.hide_cursor()?,
Some(position) => {
self.show_cursor()?;
self.set_cursor_position(position)?;
}
}
self.swap_buffers();
// Flush any buffered backend output.
self.backend.flush()?;
let completed_frame = CompletedFrame {
buffer: &self.buffers[1 - self.current],
area: self.last_known_area,
count: self.frame_count,
};
// increment frame count before returning from draw
self.frame_count = self.frame_count.wrapping_add(1);
Ok(completed_frame)
}
}
#[cfg(test)]
mod tests {
use core::fmt;
use crate::backend::{Backend, ClearType, TestBackend, WindowSize};
use crate::buffer::{Buffer, Cell};
use crate::layout::{Position, Rect};
use crate::terminal::{Terminal, TerminalOptions, Viewport};
#[derive(Debug, Clone, Eq, PartialEq)]
struct TestError(&'static str);
impl fmt::Display for TestError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl core::error::Error for TestError {}
/// A thin wrapper around [`TestBackend`] with a fallible error type.
///
/// [`TestBackend`] uses [`core::convert::Infallible`] as its associated `Backend::Error`, which
/// is ideal for most tests but makes it impossible to write a `try_draw` callback that returns
/// an error (because `E: Into<B::Error>` would require converting a real error into
/// `Infallible`). This wrapper keeps the same observable backend behavior (buffer + cursor)
/// while allowing tests to exercise `Terminal::try_draw`'s error path.
#[derive(Debug, Clone, Eq, PartialEq)]
struct FallibleTestBackend {
inner: TestBackend,
}
impl FallibleTestBackend {
fn new(inner: TestBackend) -> Self {
Self { inner }
}
}
impl Backend for FallibleTestBackend {
type Error = TestError;
fn draw<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
where
I: Iterator<Item = (u16, u16, &'a crate::buffer::Cell)>,
{
self.inner.draw(content).map_err(|err| match err {})
}
fn append_lines(&mut self, n: u16) -> Result<(), Self::Error> {
self.inner.append_lines(n).map_err(|err| match err {})
}
fn hide_cursor(&mut self) -> Result<(), Self::Error> {
self.inner.hide_cursor().map_err(|err| match err {})
}
fn show_cursor(&mut self) -> Result<(), Self::Error> {
self.inner.show_cursor().map_err(|err| match err {})
}
fn get_cursor_position(&mut self) -> Result<Position, Self::Error> {
self.inner.get_cursor_position().map_err(|err| match err {})
}
fn set_cursor_position<P: Into<Position>>(
&mut self,
position: P,
) -> Result<(), Self::Error> {
self.inner
.set_cursor_position(position)
.map_err(|err| match err {})
}
fn clear(&mut self) -> Result<(), Self::Error> {
self.inner.clear().map_err(|err| match err {})
}
fn clear_region(&mut self, clear_type: ClearType) -> Result<(), Self::Error> {
self.inner
.clear_region(clear_type)
.map_err(|err| match err {})
}
fn size(&self) -> Result<crate::layout::Size, Self::Error> {
self.inner.size().map_err(|err| match err {})
}
fn window_size(&mut self) -> Result<WindowSize, Self::Error> {
self.inner.window_size().map_err(|err| match err {})
}
fn flush(&mut self) -> Result<(), Self::Error> {
self.inner.flush().map_err(|err| match err {})
}
#[cfg(feature = "scrolling-regions")]
fn scroll_region_up(
&mut self,
region: core::ops::Range<u16>,
line_count: u16,
) -> Result<(), Self::Error> {
self.inner
.scroll_region_up(region, line_count)
.map_err(|err| match err {})
}
#[cfg(feature = "scrolling-regions")]
fn scroll_region_down(
&mut self,
region: core::ops::Range<u16>,
line_count: u16,
) -> Result<(), Self::Error> {
self.inner
.scroll_region_down(region, line_count)
.map_err(|err| match err {})
}
}
/// `draw` hides the cursor when the frame does not request a cursor position.
///
/// This asserts the end-to-end effect on the backend (buffer contents + cursor state) as well
/// as internal frame counting.
#[test]
fn draw_hides_cursor_when_frame_cursor_is_not_set() {
let backend = TestBackend::new(3, 2);
let mut terminal = Terminal::new(backend).unwrap();
terminal.show_cursor().unwrap();
let completed = terminal
.draw(|frame| {
// Ensure the frame produces updates so `Terminal::flush` writes to the backend.
frame.buffer_mut()[(0, 0)] = Cell::new("x");
})
.unwrap();
assert_eq!(completed.count, 0, "first draw returns count 0");
assert_eq!(
completed.area,
Rect::new(0, 0, 3, 2),
"completed area matches terminal size in fullscreen mode"
);
assert_eq!(
completed.buffer,
&Buffer::with_lines(["x ", " "]),
"completed buffer contains the rendered content"
);
assert!(terminal.hidden_cursor);
assert!(!terminal.backend().cursor_visible());
assert_eq!(
terminal.frame_count, 1,
"successful draw increments frame_count"
);
}
/// `draw` applies the cursor requested by `Frame::set_cursor_position`.
///
/// The cursor is updated after rendering has been flushed, so it appears on top of the drawn
/// UI.
#[test]
fn draw_shows_and_positions_cursor_when_frame_cursor_is_set() {
let backend = TestBackend::new(3, 2);
let mut terminal = Terminal::new(backend).unwrap();
terminal.hide_cursor().unwrap();
terminal
.draw(|frame| {
// The cursor is applied after the frame is flushed.
frame.set_cursor_position(Position { x: 2, y: 1 });
frame.buffer_mut()[(1, 0)] = Cell::new("y");
})
.unwrap();
assert!(!terminal.hidden_cursor);
assert!(terminal.backend().cursor_visible());
assert_eq!(
terminal.backend().cursor_position(),
Position { x: 2, y: 1 },
"backend cursor is positioned after flushing"
);
assert_eq!(
terminal.last_known_cursor_pos,
Position { x: 2, y: 1 },
"terminal cursor tracking matches the final cursor position"
);
}
/// When the render callback returns an error, `try_draw` does not update the terminal.
///
/// This is a characterization of the "no partial updates" behavior: backend contents and
/// cursor state are unchanged and `frame_count` does not advance.
#[test]
fn try_draw_propagates_render_errors_without_updating_backend() {
let backend = FallibleTestBackend::new(TestBackend::with_lines(["aaa", "bbb"]));
let mut terminal = Terminal::new(backend).unwrap();
terminal.show_cursor().unwrap();
let was_hidden = terminal.hidden_cursor;
let cursor_visible = terminal.backend().inner.cursor_visible();
let cursor_position = terminal.backend().inner.cursor_position();
let result = terminal.try_draw(|_frame| Err::<(), _>(TestError("render failed")));
assert_eq!(
result.unwrap_err(),
TestError("render failed"),
"try_draw returns the render callback error"
);
assert_eq!(terminal.frame_count, 0, "frame_count is unchanged on error");
assert_eq!(
terminal.backend().inner.buffer(),
&Buffer::with_lines(["aaa", "bbb"]),
"backend buffer is unchanged on error"
);
assert_eq!(
terminal.hidden_cursor, was_hidden,
"terminal cursor state is unchanged on error"
);
assert_eq!(
terminal.backend().inner.cursor_visible(),
cursor_visible,
"backend cursor visibility is unchanged on error"
);
assert_eq!(
terminal.backend().inner.cursor_position(),
cursor_position,
"backend cursor position is unchanged on error"
);
}
/// `draw` autoresizes fullscreen terminals and clears before rendering.
///
/// This simulates the backend resizing between draw calls; `draw` runs `autoresize()` first
/// (which calls `resize()` and clears) so the frame renders into a fresh, correctly-sized
/// region.
#[test]
fn draw_clears_on_fullscreen_resize_before_rendering() {
let backend = TestBackend::with_lines(["xxx", "yyy"]);
let mut terminal = Terminal::new(backend).unwrap();
terminal.backend_mut().resize(4, 3);
terminal
.draw(|frame| {
// Render a marker to show we rendered after the clear.
frame.buffer_mut()[(0, 0)] = Cell::new("x");
})
.unwrap();
assert_eq!(
terminal.viewport_area,
Rect::new(0, 0, 4, 3),
"viewport area tracks the resized terminal size"
);
assert_eq!(
terminal.last_known_area,
Rect::new(0, 0, 4, 3),
"last_known_area tracks the resized terminal size"
);
terminal
.backend()
.assert_buffer_lines(["x ", " ", " "]);
}
/// In fixed viewports, `Frame::area` is an absolute terminal rectangle.
///
/// This asserts that rendering at `frame.area().x/y` updates the backend at that absolute
/// position.
#[test]
fn draw_uses_fixed_viewport_coordinates() {
let backend = TestBackend::new(5, 3);
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Fixed(Rect::new(2, 1, 2, 1)),
},
)
.unwrap();
terminal
.draw(|frame| {
assert_eq!(
frame.area(),
Rect::new(2, 1, 2, 1),
"frame area matches the configured fixed viewport"
);
let area = frame.area();
frame.buffer_mut()[(area.x, area.y)] = Cell::new("z");
})
.unwrap();
terminal
.backend()
.assert_buffer_lines([" ", " z ", " "]);
}
/// Inline viewports render into a sub-rectangle, but `CompletedFrame::area` reports terminal
/// size.
///
/// This asserts that the `CompletedFrame` returned from `draw` reports the full terminal
/// size while its buffer is sized to the inline viewport, and that rendering uses the inline
/// viewport's absolute origin.
#[test]
fn draw_inline_completed_frame_reports_terminal_size() {
let mut inner = TestBackend::new(6, 5);
inner.set_cursor_position((0, 2)).unwrap();
let mut terminal = Terminal::with_options(
inner,
TerminalOptions {
viewport: Viewport::Inline(3),
},
)
.unwrap();
let viewport_area = terminal.viewport_area;
{
// `CompletedFrame` borrows the terminal, so backend assertions happen after it drops.
let completed = terminal
.draw(|frame| {
assert_eq!(
frame.area(),
viewport_area,
"inline frame area matches the computed viewport"
);
frame.buffer_mut()[(viewport_area.x, viewport_area.y)] = Cell::new("i");
})
.unwrap();
assert_eq!(
completed.area,
Rect::new(0, 0, 6, 5),
"completed area reports the full terminal size"
);
assert_eq!(
completed.buffer.area, viewport_area,
"completed buffer is sized to the inline viewport"
);
}
assert_eq!(
terminal.backend().buffer()[(viewport_area.x, viewport_area.y)].symbol(),
"i"
);
}
/// Inline viewports are autoresized during `draw`.
///
/// This asserts that when the backend reports a different terminal size, `draw` recomputes the
/// inline viewport rectangle and renders into the new viewport area.
#[test]
fn draw_inline_autoresize_recomputes_viewport_on_grow() {
let mut backend = TestBackend::new(6, 5);
backend
.set_cursor_position(Position { x: 0, y: 2 })
.unwrap();
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(3),
},
)
.unwrap();
terminal
.draw(|frame| {
let area = frame.area();
frame.set_cursor_position(Position {
x: area.x,
y: area.y.saturating_add(1),
});
frame.buffer_mut()[(area.x, area.y)] = Cell::new("a");
})
.unwrap();
terminal.backend_mut().resize(8, 7);
let new_area = Rect::new(0, 0, 8, 7);
let previous_viewport = terminal.viewport_area;
terminal
.draw(|frame| {
let area = frame.area();
frame.buffer_mut()[(area.x, area.y)] = Cell::new("g");
})
.unwrap();
assert_eq!(
terminal.last_known_area, new_area,
"inline last_known_area tracks the resized terminal size"
);
assert_eq!(
terminal.viewport_area.width, 8,
"inline viewport width tracks the resized terminal width"
);
assert_eq!(
terminal.viewport_area.height, 3,
"inline viewport height is capped by the configured inline height"
);
assert_eq!(
terminal.viewport_area.y, previous_viewport.y,
"inline viewport stays anchored relative to the cursor across a grow"
);
assert_eq!(
terminal.backend().buffer()[(terminal.viewport_area.x, terminal.viewport_area.y)]
.symbol(),
"g",
"render output lands at the recomputed viewport origin"
);
}
/// Inline viewports are autoresized during `draw`.
///
/// This asserts that shrinking the backend terminal size causes `draw` to recompute the inline
/// viewport origin so it stays visible, and that rendering uses the new viewport origin.
#[test]
fn draw_inline_autoresize_recomputes_viewport_on_shrink() {
let mut backend = TestBackend::new(6, 6);
backend
.set_cursor_position(Position { x: 0, y: 4 })
.unwrap();
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(4),
},
)
.unwrap();
terminal
.draw(|frame| {
let area = frame.area();
frame.set_cursor_position(Position {
x: area.x,
y: area.y.saturating_add(2),
});
frame.buffer_mut()[(area.x, area.y)] = Cell::new("a");
})
.unwrap();
terminal.backend_mut().resize(6, 5);
let new_area = Rect::new(0, 0, 6, 5);
terminal
.draw(|frame| {
let area = frame.area();
frame.buffer_mut()[(area.x, area.y)] = Cell::new("s");
})
.unwrap();
assert_eq!(
terminal.last_known_area, new_area,
"inline last_known_area tracks the resized terminal size"
);
assert_eq!(
terminal.viewport_area,
Rect::new(0, 1, 6, 4),
"inline viewport is recomputed to stay visible after a shrink"
);
assert_eq!(
terminal.backend().buffer()[(terminal.viewport_area.x, terminal.viewport_area.y)]
.symbol(),
"s",
"render output lands at the recomputed viewport origin"
);
}
/// `CompletedFrame` is only valid until the next draw call.
///
/// This asserts that each `draw` returns the buffer for the frame that was just rendered
/// and that the count increments after each successful draw.
#[test]
fn draw_returns_completed_frame_for_current_render_pass() {
let backend = TestBackend::new(3, 2);
let mut terminal = Terminal::new(backend).unwrap();
{
// `CompletedFrame` borrows the terminal, and is only valid until the next draw call.
let first = terminal
.draw(|frame| {
frame.buffer_mut()[(0, 0)] = Cell::new("a");
})
.unwrap();
assert_eq!(first.count, 0, "first CompletedFrame has count 0");
assert_eq!(
first.buffer,
&Buffer::with_lines(["a ", " "]),
"first frame's buffer contains the first render output"
);
}
let second = terminal
.draw(|frame| {
frame.buffer_mut()[(0, 0)] = Cell::new("b");
})
.unwrap();
assert_eq!(second.count, 1, "second CompletedFrame has count 1");
assert_eq!(
second.buffer,
&Buffer::with_lines(["b ", " "]),
"second frame's buffer contains the second render output"
);
}
}

View File

@@ -0,0 +1,255 @@
use crate::backend::Backend;
use crate::layout::Rect;
use crate::terminal::inline::compute_inline_size;
use crate::terminal::{Terminal, Viewport};
impl<B: Backend> Terminal<B> {
/// Updates the Terminal so that internal buffers match the requested area.
///
/// This updates the buffer size used for rendering and triggers a full clear so the next
/// [`Terminal::draw`] paints into a consistent area.
///
/// When the viewport is [`Viewport::Inline`], the `area` argument is treated as the new
/// terminal size and the viewport origin is recomputed relative to the current cursor position.
/// Ratatui attempts to keep the cursor at the same relative row within the viewport across
/// resizes.
///
/// See also: [`Terminal::autoresize`] (automatic resizing during [`Terminal::draw`]).
pub fn resize(&mut self, area: Rect) -> Result<(), B::Error> {
let next_area = match self.viewport {
Viewport::Inline(height) => {
let offset_in_previous_viewport = self
.last_known_cursor_pos
.y
.saturating_sub(self.viewport_area.top());
compute_inline_size(
&mut self.backend,
height,
area.as_size(),
offset_in_previous_viewport,
)?
.0
}
Viewport::Fixed(_) | Viewport::Fullscreen => area,
};
self.set_viewport_area(next_area);
self.clear()?;
self.last_known_area = area;
Ok(())
}
/// Queries the backend for size and resizes if it doesn't match the previous size.
///
/// This is called automatically during [`Terminal::draw`] for fullscreen and inline viewports.
/// Fixed viewports are not automatically resized.
///
/// If the size changed, this calls [`Terminal::resize`] (which clears the screen).
pub fn autoresize(&mut self) -> Result<(), B::Error> {
// fixed viewports do not get autoresized
if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) {
let area = self.size()?.into();
if area != self.last_known_area {
self.resize(area)?;
}
}
Ok(())
}
/// Resize internal buffers and update the current viewport area.
///
/// This is an internal helper used by [`Terminal::with_options`] and [`Terminal::resize`].
pub(crate) fn set_viewport_area(&mut self, area: Rect) {
self.buffers[self.current].resize(area);
self.buffers[1 - self.current].resize(area);
self.viewport_area = area;
}
}
#[cfg(test)]
mod tests {
use crate::backend::{Backend, TestBackend};
use crate::buffer::Buffer;
use crate::layout::{Position, Rect};
use crate::terminal::{Terminal, TerminalOptions, Viewport};
#[test]
fn resize_fullscreen_updates_viewport_and_buffer_areas() {
let backend = TestBackend::new(3, 2);
let mut terminal = Terminal::new(backend).unwrap();
terminal.backend_mut().resize(4, 3);
let new_area = Rect::new(0, 0, 4, 3);
terminal.resize(new_area).unwrap();
assert_eq!(terminal.viewport_area, new_area);
assert_eq!(terminal.last_known_area, new_area);
assert_eq!(terminal.buffers[terminal.current].area, new_area);
assert_eq!(terminal.buffers[1 - terminal.current].area, new_area);
}
#[test]
fn resize_fullscreen_triggers_clear_and_resets_back_buffer() {
// This test is specifically about the side effects of `resize`:
// - it calls `clear` to force a full redraw
// - it resets the "previous" buffer
let backend = TestBackend::new(3, 2);
let mut terminal = Terminal::new(backend).unwrap();
// Put visible content on the backend so we can tell whether a clear happened.
{
let frame = terminal.get_frame();
frame.buffer[(0, 0)].set_symbol("x");
}
terminal.flush().unwrap();
terminal.backend().assert_buffer_lines(["x ", " "]);
terminal.backend_mut().resize(4, 3);
let new_area = Rect::new(0, 0, 4, 3);
terminal.resize(new_area).unwrap();
terminal
.backend()
.assert_buffer_lines([" ", " ", " "]);
assert_eq!(
terminal.buffers[1 - terminal.current],
Buffer::empty(new_area)
);
}
#[test]
fn autoresize_fullscreen_uses_backend_size_when_changed() {
let backend = TestBackend::new(3, 2);
let mut terminal = Terminal::new(backend).unwrap();
{
let frame = terminal.get_frame();
frame.buffer[(0, 0)].set_symbol("x");
}
terminal.flush().unwrap();
terminal.backend_mut().resize(4, 3);
terminal.autoresize().unwrap();
assert_eq!(terminal.viewport_area, Rect::new(0, 0, 4, 3));
assert_eq!(terminal.last_known_area, Rect::new(0, 0, 4, 3));
terminal
.backend()
.assert_buffer_lines([" ", " ", " "]);
}
#[test]
fn autoresize_fixed_does_not_change_viewport() {
let backend = TestBackend::with_lines(["xxx", "yyy"]);
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Fixed(Rect::new(1, 0, 2, 2)),
},
)
.unwrap();
terminal.autoresize().unwrap();
assert_eq!(terminal.viewport_area, Rect::new(1, 0, 2, 2));
assert_eq!(terminal.last_known_area, Rect::new(1, 0, 2, 2));
terminal.backend().assert_buffer_lines(["xxx", "yyy"]);
}
#[test]
fn resize_fixed_changes_viewport_area_and_buffer_sizes() {
let backend = TestBackend::new(5, 3);
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Fixed(Rect::new(1, 1, 2, 1)),
},
)
.unwrap();
terminal.resize(Rect::new(0, 0, 3, 2)).unwrap();
assert_eq!(terminal.viewport_area, Rect::new(0, 0, 3, 2));
assert_eq!(terminal.last_known_area, Rect::new(0, 0, 3, 2));
assert_eq!(
terminal.buffers[terminal.current].area,
terminal.viewport_area
);
assert_eq!(
terminal.buffers[1 - terminal.current].area,
terminal.viewport_area
);
}
#[test]
fn resize_inline_recomputes_origin_using_previous_cursor_offset() {
let mut backend = TestBackend::new(10, 10);
backend
.set_cursor_position(Position { x: 0, y: 4 })
.unwrap();
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(4),
},
)
.unwrap();
assert_eq!(terminal.viewport_area, Rect::new(0, 4, 10, 4));
// Characterization test:
// This test simulates a terminal resize (increasing the terminal height) while an inline
// viewport is active. The key behavior being exercised is that the viewport remains
// anchored to the backend cursor row and preserves the cursor's relative offset within the
// previous viewport.
//
// For inline viewports, `Terminal::resize(area)` interprets `area` as the *new terminal
// size*, then recomputes the viewport origin based on:
// - the backend cursor position at the time of the call
// - the cursor offset within the *previous* viewport (`last_known_cursor_pos -
// viewport_top`)
//
// This means `resize(Rect { .. })` can update `viewport_area.y` even when the passed-in
// `area.y` is 0, because `viewport_area` is anchored to the cursor row, not the terminal
// origin.
terminal.last_known_cursor_pos = Position { x: 0, y: 5 };
terminal
.backend_mut()
.set_cursor_position(Position { x: 0, y: 6 })
.unwrap();
terminal.backend_mut().resize(10, 12);
let new_terminal_area = Rect::new(0, 0, 10, 12);
terminal.resize(new_terminal_area).unwrap();
// Previous viewport top was y=4, and last_known_cursor_pos was y=5, so the cursor offset
// within the viewport is 1 row. At the time of resize the backend cursor is at y=6, so the
// new viewport top becomes 6 - 1 = 5.
assert_eq!(terminal.viewport_area, Rect::new(0, 5, 10, 4));
assert_eq!(terminal.last_known_area, new_terminal_area);
}
#[test]
fn resize_inline_clamps_height_to_terminal_height() {
// Characterization test:
// This test simulates a terminal resize that *reduces* the terminal height. Inline
// viewports clamp their height to the new terminal size so the viewport remains fully
// visible.
let mut backend = TestBackend::new(10, 10);
backend
.set_cursor_position(Position { x: 0, y: 0 })
.unwrap();
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(10),
},
)
.unwrap();
terminal.backend_mut().resize(10, 3);
terminal.resize(Rect::new(0, 0, 10, 3)).unwrap();
assert_eq!(terminal.viewport_area, Rect::new(0, 0, 10, 3));
}
}

View File

@@ -1,926 +0,0 @@
use crate::backend::{Backend, ClearType};
use crate::buffer::{Buffer, Cell};
use crate::layout::{Position, Rect, Size};
use crate::terminal::{CompletedFrame, Frame, TerminalOptions, Viewport};
/// An interface to interact and draw [`Frame`]s on the user's terminal.
///
/// This is the main entry point for Ratatui. It is responsible for drawing and maintaining the
/// state of the buffers, cursor and viewport.
///
/// The [`Terminal`] is generic over a [`Backend`] implementation which is used to interface with
/// the underlying terminal library. The [`Backend`] trait is implemented for three popular Rust
/// terminal libraries: [Crossterm], [Termion] and [Termwiz]. See the [`backend`] module for more
/// information.
///
/// The `Terminal` struct maintains two buffers: the current and the previous.
/// When the widgets are drawn, the changes are accumulated in the current buffer.
/// At the end of each draw pass, the two buffers are compared, and only the changes
/// between these buffers are written to the terminal, avoiding any redundant operations.
/// After flushing these changes, the buffers are swapped to prepare for the next draw cycle.
///
/// The terminal also has a viewport which is the area of the terminal that is currently visible to
/// the user. It can be either fullscreen, inline or fixed. See [`Viewport`] for more information.
///
/// Applications should detect terminal resizes and call [`Terminal::draw`] to redraw the
/// application with the new size. This will automatically resize the internal buffers to match the
/// new size for inline and fullscreen viewports. Fixed viewports are not resized automatically.
///
/// # Initialization
///
/// For most applications, consider using the convenience functions `ratatui::run()`,
/// `ratatui::init()`, and `ratatui::restore()` (available since version 0.28.1) along with the
/// `DefaultTerminal` type alias instead of constructing `Terminal` instances manually. These
/// functions handle the common setup and teardown tasks automatically. Manual construction
/// using `Terminal::new()` or `Terminal::with_options()` is still supported for applications
/// that need fine-grained control over initialization.
///
/// # Examples
///
/// ## Using convenience functions (recommended for most applications)
///
/// ```rust,ignore
/// // Modern approach using convenience functions
/// ratatui::run(|terminal| {
/// terminal.draw(|frame| {
/// let area = frame.area();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// })?;
/// Ok(())
/// })?;
/// ```
///
/// ## Manual construction (for fine-grained control)
///
/// ```rust,ignore
/// use std::io::stdout;
///
/// use ratatui::{backend::CrosstermBackend, widgets::Paragraph, Terminal};
///
/// let backend = CrosstermBackend::new(stdout());
/// let mut terminal = Terminal::new(backend)?;
/// terminal.draw(|frame| {
/// let area = frame.area();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// })?;
/// # std::io::Result::Ok(())
/// ```
///
/// [Crossterm]: https://crates.io/crates/crossterm
/// [Termion]: https://crates.io/crates/termion
/// [Termwiz]: https://crates.io/crates/termwiz
/// [`backend`]: crate::backend
/// [`Backend`]: crate::backend::Backend
/// [`Buffer`]: crate::buffer::Buffer
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Terminal<B>
where
B: Backend,
{
/// The backend used to interface with the terminal
backend: B,
/// Holds the results of the current and previous draw calls. The two are compared at the end
/// of each draw pass to output the necessary updates to the terminal
buffers: [Buffer; 2],
/// Index of the current buffer in the previous array
current: usize,
/// Whether the cursor is currently hidden
hidden_cursor: bool,
/// Viewport
viewport: Viewport,
/// Area of the viewport
viewport_area: Rect,
/// Last known area of the terminal. Used to detect if the internal buffers have to be resized.
last_known_area: Rect,
/// Last known position of the cursor. Used to find the new area when the viewport is inlined
/// and the terminal resized.
last_known_cursor_pos: Position,
/// Number of frames rendered up until current time.
frame_count: usize,
}
/// Options to pass to [`Terminal::with_options`]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Options {
/// Viewport used to draw to the terminal
pub viewport: Viewport,
}
impl<B> Drop for Terminal<B>
where
B: Backend,
{
fn drop(&mut self) {
// Attempt to restore the cursor state
if self.hidden_cursor {
#[allow(unused_variables)]
if let Err(err) = self.show_cursor() {
#[cfg(feature = "std")]
std::eprintln!("Failed to show the cursor: {err}");
}
}
}
}
impl<B> Terminal<B>
where
B: Backend,
{
/// Creates a new [`Terminal`] with the given [`Backend`] with a full screen viewport.
///
/// Note that unlike `ratatui::init`, this does not install a panic hook, so it is recommended
/// to do that manually when using this function, otherwise any panic messages will be printed
/// to the alternate screen and the terminal may be left in an unusable state.
///
/// See [how to set up panic hooks](https://ratatui.rs/recipes/apps/panic-hooks/) and
/// [`better-panic` example](https://ratatui.rs/recipes/apps/better-panic/) for more
/// information.
///
/// # Example
///
/// ```rust,ignore
/// use std::io::stdout;
///
/// use ratatui::{backend::CrosstermBackend, Terminal};
///
/// let backend = CrosstermBackend::new(stdout());
/// let terminal = Terminal::new(backend)?;
///
/// // Optionally set up a panic hook to restore the terminal on panic.
/// let old_hook = std::panic::take_hook();
/// std::panic::set_hook(Box::new(move |info| {
/// ratatui::restore();
/// old_hook(info);
/// }));
/// # std::io::Result::Ok(())
/// ```
pub fn new(backend: B) -> Result<Self, B::Error> {
Self::with_options(
backend,
TerminalOptions {
viewport: Viewport::Fullscreen,
},
)
}
/// Creates a new [`Terminal`] with the given [`Backend`] and [`TerminalOptions`].
///
/// # Example
///
/// ```rust,ignore
/// use std::io::stdout;
///
/// use ratatui::{backend::CrosstermBackend, layout::Rect, Terminal, TerminalOptions, Viewport};
///
/// let backend = CrosstermBackend::new(stdout());
/// let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
/// let terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
/// # std::io::Result::Ok(())
/// ```
pub fn with_options(mut backend: B, options: TerminalOptions) -> Result<Self, B::Error> {
let area = match options.viewport {
Viewport::Fullscreen | Viewport::Inline(_) => backend.size()?.into(),
Viewport::Fixed(area) => area,
};
let (viewport_area, cursor_pos) = match options.viewport {
Viewport::Fullscreen => (area, Position::ORIGIN),
Viewport::Inline(height) => {
compute_inline_size(&mut backend, height, area.as_size(), 0)?
}
Viewport::Fixed(area) => (area, area.as_position()),
};
Ok(Self {
backend,
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
current: 0,
hidden_cursor: false,
viewport: options.viewport,
viewport_area,
last_known_area: area,
last_known_cursor_pos: cursor_pos,
frame_count: 0,
})
}
/// Get a Frame object which provides a consistent view into the terminal state for rendering.
///
/// # Note
///
/// This exists to support more advanced use cases. Most cases should be fine using
/// [`Terminal::draw`].
///
/// [`Terminal::get_frame`] should be used when you need direct access to the frame buffer
/// outside of draw closure, for example:
///
/// - Unit testing widgets
/// - Buffer state inspection
/// - Cursor manipulation
/// - Multiple rendering passes/Buffer Manipulation
/// - Custom frame lifecycle management
/// - Buffer exporting
///
/// # Example
///
/// Getting the buffer and asserting on some cells after rendering a widget.
///
/// ```rust,ignore
/// use ratatui::{backend::TestBackend, Terminal};
/// use ratatui::widgets::Paragraph;
/// let backend = TestBackend::new(30, 5);
/// let mut terminal = Terminal::new(backend).unwrap();
/// {
/// let mut frame = terminal.get_frame();
/// frame.render_widget(Paragraph::new("Hello"), frame.area());
/// }
/// // When not using `draw`, present the buffer manually:
/// terminal.flush().unwrap();
/// terminal.swap_buffers();
/// terminal.backend_mut().flush().unwrap();
/// ```
pub const fn get_frame(&mut self) -> Frame<'_> {
let count = self.frame_count;
Frame {
cursor_position: None,
viewport_area: self.viewport_area,
buffer: self.current_buffer_mut(),
count,
}
}
/// Gets the current buffer as a mutable reference.
pub const fn current_buffer_mut(&mut self) -> &mut Buffer {
&mut self.buffers[self.current]
}
/// Gets the backend
pub const fn backend(&self) -> &B {
&self.backend
}
/// Gets the backend as a mutable reference
pub const fn backend_mut(&mut self) -> &mut B {
&mut self.backend
}
/// Obtains a difference between the previous and the current buffer and passes it to the
/// current backend for drawing.
pub fn flush(&mut self) -> Result<(), B::Error> {
let previous_buffer = &self.buffers[1 - self.current];
let current_buffer = &self.buffers[self.current];
let updates = previous_buffer.diff(current_buffer);
if let Some((col, row, _)) = updates.last() {
self.last_known_cursor_pos = Position { x: *col, y: *row };
}
self.backend.draw(updates.into_iter())
}
/// Updates the Terminal so that internal buffers match the requested area.
///
/// Requested area will be saved to remain consistent when rendering. This leads to a full clear
/// of the screen.
pub fn resize(&mut self, area: Rect) -> Result<(), B::Error> {
let next_area = match self.viewport {
Viewport::Inline(height) => {
let offset_in_previous_viewport = self
.last_known_cursor_pos
.y
.saturating_sub(self.viewport_area.top());
compute_inline_size(
&mut self.backend,
height,
area.as_size(),
offset_in_previous_viewport,
)?
.0
}
Viewport::Fixed(_) | Viewport::Fullscreen => area,
};
self.set_viewport_area(next_area);
self.clear()?;
self.last_known_area = area;
Ok(())
}
fn set_viewport_area(&mut self, area: Rect) {
self.buffers[self.current].resize(area);
self.buffers[1 - self.current].resize(area);
self.viewport_area = area;
}
/// Queries the backend for size and resizes if it doesn't match the previous size.
pub fn autoresize(&mut self) -> Result<(), B::Error> {
// fixed viewports do not get autoresized
if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) {
let area = self.size()?.into();
if area != self.last_known_area {
self.resize(area)?;
}
}
Ok(())
}
/// Draws a single frame to the terminal.
///
/// Returns a [`CompletedFrame`] if successful, otherwise a [`std::io::Error`].
///
/// If the render callback passed to this method can fail, use [`try_draw`] instead.
///
/// Applications should call `draw` or [`try_draw`] in a loop to continuously render the
/// terminal. These methods are the main entry points for drawing to the terminal.
///
/// [`try_draw`]: Terminal::try_draw
///
/// This method will:
///
/// - autoresize the terminal if necessary
/// - call the render callback, passing it a [`Frame`] reference to render to
/// - flush the current internal state by copying the current buffer to the backend
/// - move the cursor to the last known position if it was set during the rendering closure
/// - return a [`CompletedFrame`] with the current buffer and the area of the terminal
///
/// The [`CompletedFrame`] returned by this method can be useful for debugging or testing
/// purposes, but it is often not used in regular applications.
///
/// The render callback should fully render the entire frame when called, including areas that
/// are unchanged from the previous frame. This is because each frame is compared to the
/// previous frame to determine what has changed, and only the changes are written to the
/// terminal. If the render callback does not fully render the frame, the terminal will not be
/// in a consistent state.
///
/// # Examples
///
/// ```rust,ignore
/// # let backend = ratatui::backend::TestBackend::new(10, 10);
/// # let mut terminal = ratatui::Terminal::new(backend)?;
/// use ratatui::{layout::Position, widgets::Paragraph};
///
/// // with a closure
/// terminal.draw(|frame| {
/// let area = frame.area();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// frame.set_cursor_position(Position { x: 0, y: 0 });
/// })?;
///
/// // or with a function
/// terminal.draw(render)?;
///
/// fn render(frame: &mut ratatui::Frame) {
/// frame.render_widget(Paragraph::new("Hello World!"), frame.area());
/// }
/// # std::io::Result::Ok(())
/// ```
pub fn draw<F>(&mut self, render_callback: F) -> Result<CompletedFrame<'_>, B::Error>
where
F: FnOnce(&mut Frame),
{
self.try_draw(|frame| {
render_callback(frame);
Ok::<(), B::Error>(())
})
}
/// Tries to draw a single frame to the terminal.
///
/// Returns [`Result::Ok`] containing a [`CompletedFrame`] if successful, otherwise
/// [`Result::Err`] containing the [`std::io::Error`] that caused the failure.
///
/// This is the equivalent of [`Terminal::draw`] but the render callback is a function or
/// closure that returns a `Result` instead of nothing.
///
/// Applications should call `try_draw` or [`draw`] in a loop to continuously render the
/// terminal. These methods are the main entry points for drawing to the terminal.
///
/// [`draw`]: Terminal::draw
///
/// This method will:
///
/// - autoresize the terminal if necessary
/// - call the render callback, passing it a [`Frame`] reference to render to
/// - flush the current internal state by copying the current buffer to the backend
/// - move the cursor to the last known position if it was set during the rendering closure
/// - return a [`CompletedFrame`] with the current buffer and the area of the terminal
///
/// The render callback passed to `try_draw` can return any [`Result`] with an error type that
/// can be converted into an [`std::io::Error`] using the [`Into`] trait. This makes it possible
/// to use the `?` operator to propagate errors that occur during rendering. If the render
/// callback returns an error, the error will be returned from `try_draw` as an
/// [`std::io::Error`] and the terminal will not be updated.
///
/// The [`CompletedFrame`] returned by this method can be useful for debugging or testing
/// purposes, but it is often not used in regular applications.
///
/// The render callback should fully render the entire frame when called, including areas that
/// are unchanged from the previous frame. This is because each frame is compared to the
/// previous frame to determine what has changed, and only the changes are written to the
/// terminal. If the render function does not fully render the frame, the terminal will not be
/// in a consistent state.
///
/// # Examples
///
/// ```ignore
/// # use ratatui::layout::Position;;
/// # let backend = ratatui::backend::TestBackend::new(10, 10);
/// # let mut terminal = ratatui::Terminal::new(backend)?;
/// use std::io;
///
/// use ratatui::widgets::Paragraph;
///
/// // with a closure
/// terminal.try_draw(|frame| {
/// let value: u8 = "not a number".parse().map_err(io::Error::other)?;
/// let area = frame.area();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// frame.set_cursor_position(Position { x: 0, y: 0 });
/// io::Result::Ok(())
/// })?;
///
/// // or with a function
/// terminal.try_draw(render)?;
///
/// fn render(frame: &mut ratatui::Frame) -> io::Result<()> {
/// let value: u8 = "not a number".parse().map_err(io::Error::other)?;
/// frame.render_widget(Paragraph::new("Hello World!"), frame.area());
/// Ok(())
/// }
/// # io::Result::Ok(())
/// ```
pub fn try_draw<F, E>(&mut self, render_callback: F) -> Result<CompletedFrame<'_>, B::Error>
where
F: FnOnce(&mut Frame) -> Result<(), E>,
E: Into<B::Error>,
{
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
// and the terminal (if growing), which may OOB.
self.autoresize()?;
let mut frame = self.get_frame();
render_callback(&mut frame).map_err(Into::into)?;
// We can't change the cursor position right away because we have to flush the frame to
// stdout first. But we also can't keep the frame around, since it holds a &mut to
// Buffer. Thus, we're taking the important data out of the Frame and dropping it.
let cursor_position = frame.cursor_position;
// Draw to stdout
self.flush()?;
match cursor_position {
None => self.hide_cursor()?,
Some(position) => {
self.show_cursor()?;
self.set_cursor_position(position)?;
}
}
self.swap_buffers();
// Flush
self.backend.flush()?;
let completed_frame = CompletedFrame {
buffer: &self.buffers[1 - self.current],
area: self.last_known_area,
count: self.frame_count,
};
// increment frame count before returning from draw
self.frame_count = self.frame_count.wrapping_add(1);
Ok(completed_frame)
}
/// Hides the cursor.
pub fn hide_cursor(&mut self) -> Result<(), B::Error> {
self.backend.hide_cursor()?;
self.hidden_cursor = true;
Ok(())
}
/// Shows the cursor.
pub fn show_cursor(&mut self) -> Result<(), B::Error> {
self.backend.show_cursor()?;
self.hidden_cursor = false;
Ok(())
}
/// Gets the current cursor position.
///
/// This is the position of the cursor after the last draw call and is returned as a tuple of
/// `(x, y)` coordinates.
#[deprecated = "use `get_cursor_position()` instead which returns `Result<Position>`"]
pub fn get_cursor(&mut self) -> Result<(u16, u16), B::Error> {
let Position { x, y } = self.get_cursor_position()?;
Ok((x, y))
}
/// Sets the cursor position.
#[deprecated = "use `set_cursor_position((x, y))` instead which takes `impl Into<Position>`"]
pub fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), B::Error> {
self.set_cursor_position(Position { x, y })
}
/// Gets the current cursor position.
///
/// This is the position of the cursor after the last draw call.
pub fn get_cursor_position(&mut self) -> Result<Position, B::Error> {
self.backend.get_cursor_position()
}
/// Sets the cursor position.
pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> Result<(), B::Error> {
let position = position.into();
self.backend.set_cursor_position(position)?;
self.last_known_cursor_pos = position;
Ok(())
}
/// Clear the terminal and force a full redraw on the next draw call.
pub fn clear(&mut self) -> Result<(), B::Error> {
match self.viewport {
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
Viewport::Inline(_) => {
self.backend
.set_cursor_position(self.viewport_area.as_position())?;
self.backend.clear_region(ClearType::AfterCursor)?;
}
Viewport::Fixed(_) => {
let area = self.viewport_area;
for y in area.top()..area.bottom() {
self.backend.set_cursor_position(Position { x: 0, y })?;
self.backend.clear_region(ClearType::AfterCursor)?;
}
}
}
// Reset the back buffer to make sure the next update will redraw everything.
self.buffers[1 - self.current].reset();
Ok(())
}
/// Clears the inactive buffer and swaps it with the current buffer
pub fn swap_buffers(&mut self) {
self.buffers[1 - self.current].reset();
self.current = 1 - self.current;
}
/// Queries the real size of the backend.
pub fn size(&self) -> Result<Size, B::Error> {
self.backend.size()
}
/// Insert some content before the current inline viewport. This has no effect when the
/// viewport is not inline.
///
/// The `draw_fn` closure will be called to draw into a writable `Buffer` that is `height`
/// lines tall. The content of that `Buffer` will then be inserted before the viewport.
///
/// If the viewport isn't yet at the bottom of the screen, inserted lines will push it towards
/// the bottom. Once the viewport is at the bottom of the screen, inserted lines will scroll
/// the area of the screen above the viewport upwards.
///
/// Before:
/// ```ignore
/// +---------------------+
/// | pre-existing line 1 |
/// | pre-existing line 2 |
/// +---------------------+
/// | viewport |
/// +---------------------+
/// | |
/// | |
/// +---------------------+
/// ```
///
/// After inserting 2 lines:
/// ```ignore
/// +---------------------+
/// | pre-existing line 1 |
/// | pre-existing line 2 |
/// | inserted line 1 |
/// | inserted line 2 |
/// +---------------------+
/// | viewport |
/// +---------------------+
/// +---------------------+
/// ```
///
/// After inserting 2 more lines:
/// ```ignore
/// +---------------------+
/// | pre-existing line 2 |
/// | inserted line 1 |
/// | inserted line 2 |
/// | inserted line 3 |
/// | inserted line 4 |
/// +---------------------+
/// | viewport |
/// +---------------------+
/// ```
///
/// If more lines are inserted than there is space on the screen, then the top lines will go
/// directly into the terminal's scrollback buffer. At the limit, if the viewport takes up the
/// whole screen, all lines will be inserted directly into the scrollback buffer.
///
/// # Examples
///
/// ## Insert a single line before the current viewport
///
/// ```rust,ignore
/// use ratatui::{
/// backend::TestBackend,
/// style::{Color, Style},
/// text::{Line, Span},
/// widgets::{Paragraph, Widget},
/// Terminal,
/// };
/// # let backend = TestBackend::new(10, 10);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// terminal.insert_before(1, |buf| {
/// Paragraph::new(Line::from(vec![
/// Span::raw("This line will be added "),
/// Span::styled("before", Style::default().fg(Color::Blue)),
/// Span::raw(" the current viewport"),
/// ]))
/// .render(buf.area, buf);
/// });
/// ```
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> Result<(), B::Error>
where
F: FnOnce(&mut Buffer),
{
match self.viewport {
#[cfg(feature = "scrolling-regions")]
Viewport::Inline(_) => self.insert_before_scrolling_regions(height, draw_fn),
#[cfg(not(feature = "scrolling-regions"))]
Viewport::Inline(_) => self.insert_before_no_scrolling_regions(height, draw_fn),
_ => Ok(()),
}
}
/// Implement `Self::insert_before` using standard backend capabilities.
#[cfg(not(feature = "scrolling-regions"))]
fn insert_before_no_scrolling_regions(
&mut self,
height: u16,
draw_fn: impl FnOnce(&mut Buffer),
) -> Result<(), B::Error> {
// The approach of this function is to first render all of the lines to insert into a
// temporary buffer, and then to loop drawing chunks from the buffer to the screen. drawing
// this buffer onto the screen.
let area = Rect {
x: 0,
y: 0,
width: self.viewport_area.width,
height,
};
let mut buffer = Buffer::empty(area);
draw_fn(&mut buffer);
let mut buffer = buffer.content.as_slice();
// Use i32 variables so we don't have worry about overflowed u16s when adding, or about
// negative results when subtracting.
let mut drawn_height: i32 = self.viewport_area.top().into();
let mut buffer_height: i32 = height.into();
let viewport_height: i32 = self.viewport_area.height.into();
let screen_height: i32 = self.last_known_area.height.into();
// The algorithm here is to loop, drawing large chunks of text (up to a screen-full at a
// time), until the remainder of the buffer plus the viewport fits on the screen. We choose
// this loop condition because it guarantees that we can write the remainder of the buffer
// with just one call to Self::draw_lines().
while buffer_height + viewport_height > screen_height {
// We will draw as much of the buffer as possible on this iteration in order to make
// forward progress. So we have:
//
// to_draw = min(buffer_height, screen_height)
//
// We may need to scroll the screen up to make room to draw. We choose the minimal
// possible scroll amount so we don't end up with the viewport sitting in the middle of
// the screen when this function is done. The amount to scroll by is:
//
// scroll_up = max(0, drawn_height + to_draw - screen_height)
//
// We want `scroll_up` to be enough so that, after drawing, we have used the whole
// screen (drawn_height - scroll_up + to_draw = screen_height). However, there might
// already be enough room on the screen to draw without scrolling (drawn_height +
// to_draw <= screen_height). In this case, we just don't scroll at all.
let to_draw = buffer_height.min(screen_height);
let scroll_up = 0.max(drawn_height + to_draw - screen_height);
self.scroll_up(scroll_up as u16)?;
buffer = self.draw_lines((drawn_height - scroll_up) as u16, to_draw as u16, buffer)?;
drawn_height += to_draw - scroll_up;
buffer_height -= to_draw;
}
// There is now enough room on the screen for the remaining buffer plus the viewport,
// though we may still need to scroll up some of the existing text first. It's possible
// that by this point we've drained the buffer, but we may still need to scroll up to make
// room for the viewport.
//
// We want to scroll up the exact amount that will leave us completely filling the screen.
// However, it's possible that the viewport didn't start on the bottom of the screen and
// the added lines weren't enough to push it all the way to the bottom. We deal with this
// case by just ensuring that our scroll amount is non-negative.
//
// We want:
// screen_height = drawn_height - scroll_up + buffer_height + viewport_height
// Or, equivalently:
// scroll_up = drawn_height + buffer_height + viewport_height - screen_height
let scroll_up = 0.max(drawn_height + buffer_height + viewport_height - screen_height);
self.scroll_up(scroll_up as u16)?;
self.draw_lines(
(drawn_height - scroll_up) as u16,
buffer_height as u16,
buffer,
)?;
drawn_height += buffer_height - scroll_up;
self.set_viewport_area(Rect {
y: drawn_height as u16,
..self.viewport_area
});
// Clear the viewport off the screen. We didn't clear earlier for two reasons. First, it
// wasn't necessary because the buffer we drew out of isn't sparse, so it overwrote
// whatever was on the screen. Second, there is a weird bug with tmux where a full screen
// clear plus immediate scrolling causes some garbage to go into the scrollback.
self.clear()?;
Ok(())
}
/// Implement `Self::insert_before` using scrolling regions.
///
/// If a terminal supports scrolling regions, it means that we can define a subset of rows of
/// the screen, and then tell the terminal to scroll up or down just within that region. The
/// rows outside of the region are not affected.
///
/// This function utilizes this feature to avoid having to redraw the viewport. This is done
/// either by splitting the screen at the top of the viewport, and then creating a gap by
/// either scrolling the viewport down, or scrolling the area above it up. The lines to insert
/// are then drawn into the gap created.
#[cfg(feature = "scrolling-regions")]
fn insert_before_scrolling_regions(
&mut self,
mut height: u16,
draw_fn: impl FnOnce(&mut Buffer),
) -> Result<(), B::Error> {
// The approach of this function is to first render all of the lines to insert into a
// temporary buffer, and then to loop drawing chunks from the buffer to the screen. drawing
// this buffer onto the screen.
let area = Rect {
x: 0,
y: 0,
width: self.viewport_area.width,
height,
};
let mut buffer = Buffer::empty(area);
draw_fn(&mut buffer);
let mut buffer = buffer.content.as_slice();
// Handle the special case where the viewport takes up the whole screen.
if self.viewport_area.height == self.last_known_area.height {
// "Borrow" the top line of the viewport. Draw over it, then immediately scroll it into
// scrollback. Do this repeatedly until the whole buffer has been put into scrollback.
let mut first = true;
while !buffer.is_empty() {
buffer = if first {
self.draw_lines(0, 1, buffer)?
} else {
self.draw_lines_over_cleared(0, 1, buffer)?
};
first = false;
self.backend.scroll_region_up(0..1, 1)?;
}
// Redraw the top line of the viewport.
let width = self.viewport_area.width as usize;
let top_line = self.buffers[1 - self.current].content[0..width].to_vec();
self.draw_lines_over_cleared(0, 1, &top_line)?;
return Ok(());
}
// Handle the case where the viewport isn't yet at the bottom of the screen.
{
let viewport_top = self.viewport_area.top();
let viewport_bottom = self.viewport_area.bottom();
let screen_bottom = self.last_known_area.bottom();
if viewport_bottom < screen_bottom {
let to_draw = height.min(screen_bottom - viewport_bottom);
self.backend
.scroll_region_down(viewport_top..viewport_bottom + to_draw, to_draw)?;
buffer = self.draw_lines_over_cleared(viewport_top, to_draw, buffer)?;
self.set_viewport_area(Rect {
y: viewport_top + to_draw,
..self.viewport_area
});
height -= to_draw;
}
}
let viewport_top = self.viewport_area.top();
while height > 0 {
let to_draw = height.min(viewport_top);
self.backend.scroll_region_up(0..viewport_top, to_draw)?;
buffer = self.draw_lines_over_cleared(viewport_top - to_draw, to_draw, buffer)?;
height -= to_draw;
}
Ok(())
}
/// Draw lines at the given vertical offset. The slice of cells must contain enough cells
/// for the requested lines. A slice of the unused cells are returned.
fn draw_lines<'a>(
&mut self,
y_offset: u16,
lines_to_draw: u16,
cells: &'a [Cell],
) -> Result<&'a [Cell], B::Error> {
let width: usize = self.last_known_area.width.into();
let (to_draw, remainder) = cells.split_at(width * lines_to_draw as usize);
if lines_to_draw > 0 {
let iter = to_draw
.iter()
.enumerate()
.map(|(i, c)| ((i % width) as u16, y_offset + (i / width) as u16, c));
self.backend.draw(iter)?;
self.backend.flush()?;
}
Ok(remainder)
}
/// Draw lines at the given vertical offset, assuming that the lines they are replacing on the
/// screen are cleared. The slice of cells must contain enough cells for the requested lines. A
/// slice of the unused cells are returned.
#[cfg(feature = "scrolling-regions")]
fn draw_lines_over_cleared<'a>(
&mut self,
y_offset: u16,
lines_to_draw: u16,
cells: &'a [Cell],
) -> Result<&'a [Cell], B::Error> {
let width: usize = self.last_known_area.width.into();
let (to_draw, remainder) = cells.split_at(width * lines_to_draw as usize);
if lines_to_draw > 0 {
let area = Rect::new(0, y_offset, width as u16, y_offset + lines_to_draw);
let old = Buffer::empty(area);
let new = Buffer {
area,
content: to_draw.to_vec(),
};
self.backend.draw(old.diff(&new).into_iter())?;
self.backend.flush()?;
}
Ok(remainder)
}
/// Scroll the whole screen up by the given number of lines.
#[cfg(not(feature = "scrolling-regions"))]
fn scroll_up(&mut self, lines_to_scroll: u16) -> Result<(), B::Error> {
if lines_to_scroll > 0 {
self.set_cursor_position(Position::new(
0,
self.last_known_area.height.saturating_sub(1),
))?;
self.backend.append_lines(lines_to_scroll)?;
}
Ok(())
}
}
fn compute_inline_size<B: Backend>(
backend: &mut B,
height: u16,
size: Size,
offset_in_previous_viewport: u16,
) -> Result<(Rect, Position), B::Error> {
let pos = backend.get_cursor_position()?;
let mut row = pos.y;
let max_height = size.height.min(height);
let lines_after_cursor = height
.saturating_sub(offset_in_previous_viewport)
.saturating_sub(1);
backend.append_lines(lines_after_cursor)?;
let available_lines = size.height.saturating_sub(row).saturating_sub(1);
let missing_lines = lines_after_cursor.saturating_sub(available_lines);
if missing_lines > 0 {
row = row.saturating_sub(missing_lines);
}
row = row.saturating_sub(offset_in_previous_viewport);
Ok((
Rect {
x: 0,
y: row,
width: size.width,
height: max_height,
},
pos,
))
}

View File

@@ -2,31 +2,67 @@ use core::fmt;
use crate::layout::Rect;
/// Represents the viewport of the terminal. The viewport is the area of the terminal that is
/// currently visible to the user. It can be either fullscreen, inline or fixed.
/// The area of the terminal that Ratatui draws into.
///
/// When the viewport is fullscreen, the whole terminal is used to draw the application.
/// A [`Viewport`] controls where widgets render and what [`Frame::area`] returns.
///
/// When the viewport is inline, it is drawn inline with the rest of the terminal. The height of
/// the viewport is fixed, but the width is the same as the terminal width.
/// For a higher-level overview of viewports in the context of an application (including
/// examples), see [`Terminal`].
///
/// When the viewport is fixed, it is drawn in a fixed area of the terminal. The area is specified
/// by a [`Rect`].
/// Most applications use [`Viewport::Fullscreen`]. Use [`Viewport::Inline`] when you want to embed
/// a UI into a larger CLI flow (for example: print some text, then start an interactive UI below
/// it). Use [`Viewport::Fixed`] when you want Ratatui to render into a specific region of the
/// terminal.
///
/// See [`Terminal::with_options`] for more information.
/// In fullscreen mode, the viewport starts at (0, 0). In inline and fixed mode, the viewport may
/// have a non-zero `x`/`y` origin; prefer using `Frame::area()` as your root layout rectangle.
///
/// See [`Terminal::with_options`] for how to select a viewport, and [`Terminal::resize`] /
/// [`Terminal::autoresize`] for resize behavior.
///
/// [`Frame::area`]: crate::terminal::Frame::area
/// [`Terminal`]: crate::terminal::Terminal
/// [`Terminal::with_options`]: crate::terminal::Terminal::with_options
/// [`Terminal::resize`]: crate::terminal::Terminal::resize
/// [`Terminal::autoresize`]: crate::terminal::Terminal::autoresize
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub enum Viewport {
/// The viewport is fullscreen
/// Draw into the entire terminal.
///
/// This is the default viewport used by [`Terminal::new`].
///
/// When the terminal size changes, Ratatui automatically resizes internal buffers during
/// [`Terminal::draw`].
///
/// `Frame::area()` always starts at (0, 0).
///
/// [`Terminal::new`]: crate::terminal::Terminal::new
/// [`Terminal::draw`]: crate::terminal::Terminal::draw
#[default]
Fullscreen,
/// The viewport is inline with the rest of the terminal.
/// Draw the application inline with the rest of the terminal output.
///
/// The viewport's height is fixed and specified in number of lines. The width is the same as
/// the terminal's width. The viewport is drawn below the cursor position.
/// The viewport spans the full terminal width and its top-left corner is anchored to column 0
/// of the current cursor row when the terminal is created (and when it is resized). Ratatui
/// reserves space for the requested height; if the cursor is near the bottom of the screen,
/// this may scroll the terminal so the viewport remains fully visible.
///
/// The height is specified in rows and is clamped to the current terminal height.
Inline(u16),
/// The viewport is drawn in a fixed area of the terminal. The area is specified by a [`Rect`].
/// Draw into a fixed region of the terminal.
///
/// This can be useful when Ratatui is responsible for only part of the screen (for example, a
/// status panel beside another renderer), or when you want to manage the overall layout
/// yourself.
///
/// Fixed viewports are not automatically resized. If the region should change (for example, on
/// terminal resize), call [`Terminal::resize`] yourself.
///
/// The area is specified as a [`Rect`] in terminal coordinates.
///
/// `Frame::area()` returns this rectangle as-is (including its `x`/`y` offset).
///
/// [`Terminal::resize`]: crate::terminal::Terminal::resize
Fixed(Rect),
}

View File

@@ -803,7 +803,7 @@ fn spans_after_width<'a>(
/// A trait for converting a value to a [`Line`].
///
/// This trait is automatically implemented for any type that implements the [`Display`] trait. As
/// such, `ToLine` shouln't be implemented directly: [`Display`] should be implemented instead, and
/// such, `ToLine` shouldn't be implemented directly: [`Display`] should be implemented instead, and
/// you get the `ToLine` implementation for free.
///
/// [`Display`]: std::fmt::Display

View File

@@ -474,7 +474,7 @@ impl Widget for &Span<'_> {
/// 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
/// such, `ToSpan` shouldn't be implemented directly: [`Display`] should be implemented instead, and
/// you get the `ToSpan` implementation for free.
///
/// [`Display`]: std::fmt::Display

View File

@@ -423,6 +423,79 @@ impl IntoCrossterm<CrosstermColor> for Color {
}
}
impl IntoCrossterm<ContentStyle> for Style {
fn into_crossterm(self) -> ContentStyle {
let mut attributes = CrosstermAttributes::default();
// Add modifiers
if self.add_modifier.contains(Modifier::BOLD) {
attributes.set(CrosstermAttribute::Bold);
}
if self.add_modifier.contains(Modifier::DIM) {
attributes.set(CrosstermAttribute::Dim);
}
if self.add_modifier.contains(Modifier::ITALIC) {
attributes.set(CrosstermAttribute::Italic);
}
if self.add_modifier.contains(Modifier::UNDERLINED) {
attributes.set(CrosstermAttribute::Underlined);
}
if self.add_modifier.contains(Modifier::SLOW_BLINK) {
attributes.set(CrosstermAttribute::SlowBlink);
}
if self.add_modifier.contains(Modifier::RAPID_BLINK) {
attributes.set(CrosstermAttribute::RapidBlink);
}
if self.add_modifier.contains(Modifier::REVERSED) {
attributes.set(CrosstermAttribute::Reverse);
}
if self.add_modifier.contains(Modifier::HIDDEN) {
attributes.set(CrosstermAttribute::Hidden);
}
if self.add_modifier.contains(Modifier::CROSSED_OUT) {
attributes.set(CrosstermAttribute::CrossedOut);
}
// Sub modifiers (remove modifiers)
if self.sub_modifier.contains(Modifier::BOLD) {
attributes.set(CrosstermAttribute::NoBold);
}
if self.sub_modifier.contains(Modifier::DIM) {
attributes.set(CrosstermAttribute::NormalIntensity);
}
if self.sub_modifier.contains(Modifier::ITALIC) {
attributes.set(CrosstermAttribute::NoItalic);
}
if self.sub_modifier.contains(Modifier::UNDERLINED) {
attributes.set(CrosstermAttribute::NoUnderline);
}
if self.sub_modifier.contains(Modifier::SLOW_BLINK)
|| self.sub_modifier.contains(Modifier::RAPID_BLINK)
{
attributes.set(CrosstermAttribute::NoBlink);
}
if self.sub_modifier.contains(Modifier::REVERSED) {
attributes.set(CrosstermAttribute::NoReverse);
}
if self.sub_modifier.contains(Modifier::HIDDEN) {
attributes.set(CrosstermAttribute::NoHidden);
}
if self.sub_modifier.contains(Modifier::CROSSED_OUT) {
attributes.set(CrosstermAttribute::NotCrossedOut);
}
ContentStyle {
foreground_color: self.fg.map(IntoCrossterm::into_crossterm),
background_color: self.bg.map(IntoCrossterm::into_crossterm),
#[cfg(feature = "underline-color")]
underline_color: self.underline_color.map(IntoCrossterm::into_crossterm),
#[cfg(not(feature = "underline-color"))]
underline_color: None,
attributes,
}
}
}
impl FromCrossterm<CrosstermColor> for Color {
fn from_crossterm(value: CrosstermColor) -> Self {
match value {
@@ -876,4 +949,176 @@ mod tests {
Style::default().underline_color(Color::Red)
);
}
#[rstest]
#[case(Style::default(), ContentStyle::default())]
#[case(
Style::default().fg(Color::Yellow),
ContentStyle {
foreground_color: Some(CrosstermColor::DarkYellow),
..Default::default()
}
)]
#[case(
Style::default().bg(Color::Yellow),
ContentStyle {
background_color: Some(CrosstermColor::DarkYellow),
..Default::default()
}
)]
#[case(
Style::default().add_modifier(Modifier::BOLD),
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::Bold),
..Default::default()
}
)]
#[case(
Style::default().remove_modifier(Modifier::BOLD),
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::NoBold),
..Default::default()
}
)]
#[case(
Style::default().add_modifier(Modifier::ITALIC),
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::Italic),
..Default::default()
}
)]
#[case(
Style::default().remove_modifier(Modifier::ITALIC),
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::NoItalic),
..Default::default()
}
)]
#[case(
Style::default().add_modifier(Modifier::UNDERLINED),
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::Underlined),
..Default::default()
}
)]
#[case(
Style::default().remove_modifier(Modifier::UNDERLINED),
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::NoUnderline),
..Default::default()
}
)]
#[case(
Style::default().add_modifier(Modifier::DIM),
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::Dim),
..Default::default()
}
)]
#[case(
Style::default().remove_modifier(Modifier::DIM),
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::NormalIntensity),
..Default::default()
}
)]
#[case(
Style::default().add_modifier(Modifier::SLOW_BLINK),
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::SlowBlink),
..Default::default()
}
)]
#[case(
Style::default().add_modifier(Modifier::RAPID_BLINK),
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::RapidBlink),
..Default::default()
}
)]
#[case(
Style::default().remove_modifier(Modifier::SLOW_BLINK),
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::NoBlink),
..Default::default()
}
)]
#[case(
Style::default().add_modifier(Modifier::REVERSED),
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::Reverse),
..Default::default()
}
)]
#[case(
Style::default().remove_modifier(Modifier::REVERSED),
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::NoReverse),
..Default::default()
}
)]
#[case(
Style::default().add_modifier(Modifier::HIDDEN),
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::Hidden),
..Default::default()
}
)]
#[case(
Style::default().remove_modifier(Modifier::HIDDEN),
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::NoHidden),
..Default::default()
}
)]
#[case(
Style::default().add_modifier(Modifier::CROSSED_OUT),
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::CrossedOut),
..Default::default()
}
)]
#[case(
Style::default().remove_modifier(Modifier::CROSSED_OUT),
ContentStyle {
attributes: CrosstermAttributes::from(CrosstermAttribute::NotCrossedOut),
..Default::default()
}
)]
#[case(
Style::default()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::ITALIC),
ContentStyle {
attributes: CrosstermAttributes::from(
[CrosstermAttribute::Bold, CrosstermAttribute::Italic].as_ref()
),
..Default::default()
}
)]
#[case(
Style::default()
.remove_modifier(Modifier::BOLD)
.remove_modifier(Modifier::ITALIC),
ContentStyle {
attributes: CrosstermAttributes::from(
[CrosstermAttribute::NoBold, CrosstermAttribute::NoItalic].as_ref()
),
..Default::default()
}
)]
fn into_crossterm_content_style(#[case] style: Style, #[case] content_style: ContentStyle) {
assert_eq!(style.into_crossterm(), content_style);
}
#[test]
#[cfg(feature = "underline-color")]
fn into_crossterm_content_style_underline() {
let style = Style::default().underline_color(Color::Red);
let content_style = ContentStyle {
underline_color: Some(CrosstermColor::DarkRed),
..Default::default()
};
assert_eq!(style.into_crossterm(), content_style);
}
}

View File

@@ -0,0 +1,111 @@
/// Implement `AsRef<Self>` for widget types to enable `as_ref()` in generic contexts.
///
/// This keeps widget rendering ergonomic when APIs accept `AsRef<WidgetType>` bounds, avoiding
/// the need for `(&widget).render(...)` just to satisfy a trait bound.
///
/// # Example
///
/// ```rust
/// use ratatui_widgets::block::Block;
///
/// let block = Block::default();
/// let block_ref: &Block<'_> = block.as_ref();
/// ```
///
/// # Generated impls
///
/// ```rust,ignore
/// // Non-generic widgets (e.g. Clear, RatatuiLogo).
/// impl AsRef<Clear> for Clear {
/// fn as_ref(&self) -> &Clear {
/// self
/// }
/// }
///
/// // Generic widgets (e.g. Block with a lifetime, Canvas with a lifetime + type parameter).
/// impl<'a> AsRef<Block<'a>> for Block<'a> {
/// fn as_ref(&self) -> &Block<'a> {
/// self
/// }
/// }
///
/// impl<'a, F> AsRef<Canvas<'a, F>> for Canvas<'a, F>
/// where
/// F: Fn(&mut Context),
/// {
/// fn as_ref(&self) -> &Canvas<'a, F> {
/// self
/// }
/// }
/// ```
macro_rules! impl_as_ref {
($type:ty, <$($gen:tt),+> $(where $($bounds:tt)+)?) => {
impl<$($gen),+> AsRef<$type> for $type $(where $($bounds)+)? {
fn as_ref(&self) -> &$type {
self
}
}
};
($type:ty) => {
impl AsRef<$type> for $type {
fn as_ref(&self) -> &$type {
self
}
}
};
}
impl_as_ref!(crate::barchart::BarChart<'a>, <'a>);
impl_as_ref!(crate::block::Block<'a>, <'a>);
impl_as_ref!(crate::canvas::Canvas<'a, F>, <'a, F> where F: Fn(&mut crate::canvas::Context));
impl_as_ref!(crate::chart::Chart<'a>, <'a>);
impl_as_ref!(crate::clear::Clear);
impl_as_ref!(crate::gauge::Gauge<'a>, <'a>);
impl_as_ref!(crate::gauge::LineGauge<'a>, <'a>);
impl_as_ref!(crate::list::List<'a>, <'a>);
impl_as_ref!(crate::logo::RatatuiLogo);
impl_as_ref!(crate::mascot::RatatuiMascot);
impl_as_ref!(crate::paragraph::Paragraph<'a>, <'a>);
impl_as_ref!(crate::scrollbar::Scrollbar<'a>, <'a>);
impl_as_ref!(crate::sparkline::Sparkline<'a>, <'a>);
impl_as_ref!(crate::table::Table<'a>, <'a>);
impl_as_ref!(crate::tabs::Tabs<'a>, <'a>);
#[cfg(feature = "calendar")]
impl_as_ref!(
crate::calendar::Monthly<'a, DS>,
<'a, DS> where DS: crate::calendar::DateStyler
);
#[cfg(test)]
mod tests {
use alloc::vec;
#[test]
fn widgets_implement_as_ref() {
let _ = crate::barchart::BarChart::default().as_ref();
let _ = crate::block::Block::new().as_ref();
let _ = crate::canvas::Canvas::default().paint(|_| {}).as_ref();
let _ = crate::chart::Chart::new(vec![]).as_ref();
let _ = crate::clear::Clear.as_ref();
let _ = crate::gauge::Gauge::default().as_ref();
let _ = crate::gauge::LineGauge::default().as_ref();
let _ = crate::list::List::new(["foo"]).as_ref();
let _ = crate::logo::RatatuiLogo::default().as_ref();
let _ = crate::mascot::RatatuiMascot::default().as_ref();
let _ = crate::paragraph::Paragraph::new("").as_ref();
let _ = crate::scrollbar::Scrollbar::default().as_ref();
let _ = crate::sparkline::Sparkline::default().as_ref();
let _ = crate::table::Table::default().as_ref();
let _ = crate::tabs::Tabs::default().as_ref();
}
#[cfg(feature = "calendar")]
#[test]
fn calendar_widget_implements_as_ref() {
use time::{Date, Month};
let date = Date::from_calendar_date(2024, Month::January, 1).unwrap();
let _ = crate::calendar::Monthly::new(date, crate::calendar::CalendarEventStore::default())
.as_ref();
}
}

View File

@@ -31,7 +31,7 @@ impl MapResolution {
}
}
/// A world map
/// A world map. It represents the world using the [EPSG:4326 coordinate reference system](https://en.wikipedia.org/wiki/EPSG_Geodetic_Parameter_Dataset).
///
/// A world map can be rendered with different [resolutions](MapResolution) and [colors](Color).
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]

View File

@@ -130,5 +130,6 @@ pub mod tabs;
mod polyfills;
mod reflow;
mod as_ref;
#[cfg(feature = "calendar")]
pub mod calendar;

View File

@@ -403,7 +403,7 @@ impl<'a> List<'a> {
///
/// # Example
///
/// A padding value of 1 will keep 1 item above and 1 item bellow visible if possible
/// A padding value of 1 will keep 1 item above and 1 item below visible if possible
///
/// ```rust
/// use ratatui::widgets::List;

View File

@@ -220,7 +220,7 @@ impl List<'_> {
let last_valid_index = self.items.len().saturating_sub(1);
let selected = selected?.min(last_valid_index);
// The bellow loop handles situations where the list item sizes may not be consistent,
// The below loop handles situations where the list item sizes may not be consistent,
// where the offset would have excluded some items that we want to include, or could
// cause the offset value to be set to an inconsistent value each time we render.
// The padding value will be reduced in case any of these issues would occur
@@ -1219,7 +1219,7 @@ mod tests {
assert_eq!(buffer, Buffer::with_lines(expected));
}
// Tests to make sure when it's pushing back the first visible index value that it doesnt
// Tests to make sure when it's pushing back the first visible index value that it doesn't
// include an item that's too large
#[test]
fn padding_offset_pushback_break() {

View File

@@ -776,7 +776,7 @@ impl StatefulWidget for &Table<'_> {
self.render_header(header_area, buf, &column_widths);
self.render_rows(rows_area, buf, state, selection_width, &column_widths);
self.render_rows(rows_area, buf, selection_width, state, &column_widths);
self.render_footer(footer_area, buf, &column_widths);
}
@@ -806,31 +806,47 @@ impl Table<'_> {
(header_area, rows_area, footer_area)
}
fn render_header(&self, area: Rect, buf: &mut Buffer, column_widths: &[(u16, u16)]) {
/// Render the header cells, if they are not `None`
///
/// The `x` and `width` fields of each `Rect` in `column_widths` denote the starting
/// x-coordinate and width of each column in the table.
fn render_header(&self, area: Rect, buf: &mut Buffer, column_widths: &[Rect]) {
if let Some(ref header) = self.header {
buf.set_style(area, header.style);
for ((x, width), cell) in column_widths.iter().zip(header.cells.iter()) {
cell.render(Rect::new(area.x + x, area.y, *width, area.height), buf);
for (cell_area, cell) in column_widths.iter().zip(header.cells.iter()) {
let new_x = area.x + cell_area.x;
let area_to_render = Rect::new(new_x, area.y, cell_area.width, area.height);
cell.render(area_to_render, buf);
}
}
}
fn render_footer(&self, area: Rect, buf: &mut Buffer, column_widths: &[(u16, u16)]) {
/// Render the footer cells, if they are not `None`
///
/// The `x` and `width` fields of each `Rect` in `column_widths` denote the starting
/// x-coordinate and width of each column in the table.
fn render_footer(&self, area: Rect, buf: &mut Buffer, column_widths: &[Rect]) {
if let Some(ref footer) = self.footer {
buf.set_style(area, footer.style);
for ((x, width), cell) in column_widths.iter().zip(footer.cells.iter()) {
cell.render(Rect::new(area.x + x, area.y, *width, area.height), buf);
for (cell_area, cell) in column_widths.iter().zip(footer.cells.iter()) {
let new_x = area.x + cell_area.x;
let area_to_render = Rect::new(new_x, area.y, cell_area.width, area.height);
cell.render(area_to_render, buf);
}
}
}
/// Render the table rows
///
/// The `x` and `width` fields of each `Rect` in `column_widths` denote the starting
/// x-coordinate and width of each column in the table.
fn render_rows(
&self,
area: Rect,
buf: &mut Buffer,
state: &mut TableState,
selection_width: u16,
columns_widths: &[(u16, u16)],
state: &mut TableState,
columns_widths: &[Rect],
) {
if self.rows.is_empty() {
return;
@@ -856,19 +872,9 @@ impl Table<'_> {
let is_selected = state.selected.is_some_and(|index| index == i);
if selection_width > 0 && is_selected {
let selection_area = Rect {
width: selection_width,
..row_area
};
buf.set_style(selection_area, row.style);
(&self.highlight_symbol).render(selection_area, buf);
}
for ((x, width), cell) in columns_widths.iter().zip(row.cells.iter()) {
cell.render(
Rect::new(row_area.x + x, row_area.y, *width, row_area.height),
buf,
);
self.set_selection_style(buf, selection_width, row_area, row);
}
self.render_row_cells(buf, columns_widths.iter().collect(), &row.cells, row_area);
if is_selected {
selected_row_area = Some(row_area);
}
@@ -878,9 +884,9 @@ impl Table<'_> {
let selected_column_area = state.selected_column.and_then(|s| {
// The selection is clamped by the column count. Since a user can manually specify an
// incorrect number of widths, we should use panic free methods.
columns_widths.get(s).map(|(x, width)| Rect {
x: x + area.x,
width: *width,
columns_widths.get(s).map(|cell_area| Rect {
x: cell_area.x + area.x,
width: cell_area.width,
..area
})
});
@@ -902,6 +908,83 @@ impl Table<'_> {
}
}
/// Render cells into the columns of a row
///
/// Render `Cell`s from `cells` into columns specified by `column_widths`, stopping
/// if either of these iterators are finished. Each `Cell` gets rendered across
/// [`Cell::get_column_span`] columns plus the gaps between them, if this value is > 1.
fn render_row_cells(
&self,
buf: &mut Buffer,
column_widths: Vec<&Rect>,
cells: &Vec<Cell>,
row_area: Rect,
) {
let mut column_widths_iterator = column_widths.into_iter();
for current_cell in cells {
if let Some(cell_area) = Self::get_cell_area(
&mut column_widths_iterator,
current_cell.column_span,
self.column_spacing,
) {
let new_x = row_area.x + cell_area.x;
let area_to_render = Rect::new(new_x, row_area.y, cell_area.width, row_area.height);
current_cell.render(area_to_render, buf);
}
}
}
/// Set the row style and render the highlight symbol
fn set_selection_style(
&self,
buf: &mut Buffer,
selection_width: u16,
row_area: Rect,
row: &Row,
) {
let selection_area = Rect {
width: selection_width,
..row_area
};
buf.set_style(selection_area, row.style);
(&self.highlight_symbol).render(selection_area, buf);
}
/// Return the area that a [`Cell`] should occupy, taking into account its
/// [`Cell::column_span`].
///
/// Returns `None` when there are no more columns for the [`Cell`] to occupy.
///
/// Otherwise, returns `Some(Rect{x, y = 0, width, height = 0})`, representing the start
/// x-coordinate and width of the [`Cell`].
///
/// This function consumes `cell_column_span` `Rect`s from `column_widths_iterator` (or all the
/// `Rects` if the iterator is less than `cell_column_span` `Rect`s long). This function adds
/// the width of each `Rect` plus `column_spacing` to a running total of the final width. The
/// return value is the original x coordinate and the final width, or `None` if
/// `column_widths_iterator` is empty or `cell_column_span` is `0`.
fn get_cell_area<'a, T>(
column_widths_iterator: &mut T,
cell_column_span: u16,
column_spacing: u16,
) -> Option<Rect>
where
T: Iterator<Item = &'a Rect>,
{
if cell_column_span == 0 {
return None;
}
let first = column_widths_iterator.next()?;
let (n_columns_taken, all_columns_width) = column_widths_iterator
.take((cell_column_span - 1).into())
.map(|rect| (1, rect.width))
.fold((1, first.width), |so_far, next_column| {
(next_column.0 + so_far.0, next_column.1 + so_far.1)
});
let width = all_columns_width + (n_columns_taken - 1) * column_spacing;
Some(Rect::new(first.x, first.y, width, 1))
}
/// Return the indexes of the visible rows.
///
/// The algorithm works as follows:
@@ -960,7 +1043,7 @@ impl Table<'_> {
max_width: u16,
selection_width: u16,
col_count: usize,
) -> Vec<(u16, u16)> {
) -> Vec<Rect> {
let widths = if self.widths.is_empty() {
// Divide the space between each column equally
vec![Constraint::Length(max_width / col_count.max(1) as u16); col_count]
@@ -975,7 +1058,10 @@ impl Table<'_> {
.flex(self.flex)
.spacing(self.column_spacing)
.split(columns_area);
rects.iter().map(|c| (c.x, c.width)).collect()
rects
.iter()
.map(|c| Rect::new(c.x, 0, c.width, 1))
.collect()
}
fn column_count(&self) -> usize {
@@ -1352,6 +1438,101 @@ mod tests {
assert_eq!(buf, expected);
}
#[rstest]
#[case(15, 5, vec![
Row::new(vec![
Cell::new("Cell1").column_span(1),
Cell::new("Cell2").column_span(1),
]),
Row::new(vec![
Cell::new("Cell3").column_span(1),
Cell::new("Cell4").column_span(1),
]),
],
&Buffer::with_lines(["Cell1 Cell2 ", "Cell3 Cell4 "]))]
#[case(15, 5, vec![
Row::new(vec![
Cell::new("Cell1").column_span(0),
Cell::new("Cell2").column_span(1),
]),
Row::new(vec![
Cell::new("Cell3").column_span(1),
Cell::new("Cell4").column_span(1),
]),
], &Buffer::with_lines(["Cell2 ", "Cell3 Cell4 "]))]
#[case(15, 5, vec![
Row::new(vec![
Cell::new("Cell1").column_span(2),
Cell::new("Cell2").column_span(1),
]),
Row::new(vec![
Cell::new("Cell3").column_span(1),
Cell::new("Cell4").column_span(1),
]),
], &Buffer::with_lines(["Cell1 ", "Cell3 Cell4 "]))]
fn test_colspans_2_cols<'rows, Rows>(
#[case] width: u16,
#[case] column_width: u16,
#[case] rows: Rows,
#[case] expected: &Buffer,
) where
Rows: IntoIterator<Item = Row<'rows>>,
{
let mut buf = Buffer::empty(Rect::new(0, 0, width, 2));
let table = Table::new(rows, [Constraint::Length(column_width); 2]);
Widget::render(table, Rect::new(0, 0, width, 2), &mut buf);
assert_eq!(buf, *expected);
}
#[rstest]
#[case(17, 5, vec![
Row::new(vec![
Cell::new("Cell1").column_span(2),
Cell::new("Cell2").column_span(1),
]),
Row::new(vec![
Cell::new("Cell3").column_span(1),
Cell::new("Cell4").column_span(1),
Cell::new("Cell5").column_span(1),
]),
], &Buffer::with_lines(["Cell1 Cell2", "Cell3 Cell4 Cell5"]))]
#[case(17, 5, vec![
Row::new(vec![
Cell::new("Cell1").column_span(1),
Cell::new("Cell2").column_span(2),
Cell::new("Cell3").column_span(1),
]),
Row::new(vec![
Cell::new("Cell4").column_span(1),
Cell::new("Cell5").column_span(1),
Cell::new("Cell6").column_span(1),
]),
], &Buffer::with_lines(["Cell1 Cell2 ", "Cell4 Cell5 Cell6"]))]
#[case(15, 5, vec![
Row::new(vec![
Cell::new("11111111111111111111").column_span(2),
Cell::new("22222222222222222222").column_span(1),
]),
Row::new(vec![
Cell::new("33333333333333333333").column_span(1),
Cell::new("44444444444444444444").column_span(2),
Cell::new("55555555555555555555").column_span(1),
]),
], &Buffer::with_lines(["1111111111 2222", "3333 4444444444"]))]
fn test_colspans_3_cols<'rows, Rows>(
#[case] width: u16,
#[case] column_width: u16,
#[case] rows: Rows,
#[case] expected: &Buffer,
) where
Rows: IntoIterator<Item = Row<'rows>>,
{
let mut buf = Buffer::empty(Rect::new(0, 0, width, 2));
let table = Table::new(rows, [Constraint::Length(column_width); 3]);
Widget::render(table, Rect::new(0, 0, width, 2), &mut buf);
assert_eq!(buf, *expected);
}
#[test]
fn render_with_header() {
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
@@ -1676,15 +1857,24 @@ mod tests {
fn length_constraint() {
// without selection, more than needed width
let table = Table::default().widths([Length(4), Length(4)]);
assert_eq!(table.get_column_widths(20, 0, 0), [(0, 4), (5, 4)]);
assert_eq!(
table.get_column_widths(20, 0, 0),
[Rect::new(0, 0, 4, 1), Rect::new(5, 0, 4, 1),]
);
// with selection, more than needed width
let table = Table::default().widths([Length(4), Length(4)]);
assert_eq!(table.get_column_widths(20, 3, 0), [(3, 4), (8, 4)]);
assert_eq!(
table.get_column_widths(20, 3, 0),
[Rect::new(3, 0, 4, 1), Rect::new(8, 0, 4, 1)]
);
// without selection, less than needed width
let table = Table::default().widths([Length(4), Length(4)]);
assert_eq!(table.get_column_widths(7, 0, 0), [(0, 3), (4, 3)]);
assert_eq!(
table.get_column_widths(7, 0, 0),
[Rect::new(0, 0, 3, 1), Rect::new(4, 0, 3, 1)]
);
// with selection, less than needed width
// <--------7px-------->
@@ -1693,26 +1883,41 @@ mod tests {
// └────────┘x└────────┘
// column spacing (i.e. `x`) is always prioritized
let table = Table::default().widths([Length(4), Length(4)]);
assert_eq!(table.get_column_widths(7, 3, 0), [(3, 2), (6, 1)]);
assert_eq!(
table.get_column_widths(7, 3, 0),
[Rect::new(3, 0, 2, 1), Rect::new(6, 0, 1, 1)]
);
}
#[test]
fn max_constraint() {
// without selection, more than needed width
let table = Table::default().widths([Max(4), Max(4)]);
assert_eq!(table.get_column_widths(20, 0, 0), [(0, 4), (5, 4)]);
assert_eq!(
table.get_column_widths(20, 0, 0),
[Rect::new(0, 0, 4, 1), Rect::new(5, 0, 4, 1)]
);
// with selection, more than needed width
let table = Table::default().widths([Max(4), Max(4)]);
assert_eq!(table.get_column_widths(20, 3, 0), [(3, 4), (8, 4)]);
assert_eq!(
table.get_column_widths(20, 3, 0),
[Rect::new(3, 0, 4, 1), Rect::new(8, 0, 4, 1)]
);
// without selection, less than needed width
let table = Table::default().widths([Max(4), Max(4)]);
assert_eq!(table.get_column_widths(7, 0, 0), [(0, 3), (4, 3)]);
assert_eq!(
table.get_column_widths(7, 0, 0),
[Rect::new(0, 0, 3, 1), Rect::new(4, 0, 3, 1)]
);
// with selection, less than needed width
let table = Table::default().widths([Max(4), Max(4)]);
assert_eq!(table.get_column_widths(7, 3, 0), [(3, 2), (6, 1)]);
assert_eq!(
table.get_column_widths(7, 3, 0),
[Rect::new(3, 0, 2, 1), Rect::new(6, 0, 1, 1)]
);
}
#[test]
@@ -1723,42 +1928,66 @@ mod tests {
// without selection, more than needed width
let table = Table::default().widths([Min(4), Min(4)]);
assert_eq!(table.get_column_widths(20, 0, 0), [(0, 10), (11, 9)]);
assert_eq!(
table.get_column_widths(20, 0, 0),
[Rect::new(0, 0, 10, 1), Rect::new(11, 0, 9, 1)]
);
// with selection, more than needed width
let table = Table::default().widths([Min(4), Min(4)]);
assert_eq!(table.get_column_widths(20, 3, 0), [(3, 8), (12, 8)]);
assert_eq!(
table.get_column_widths(20, 3, 0),
[Rect::new(3, 0, 8, 1), Rect::new(12, 0, 8, 1)]
);
// without selection, less than needed width
// allocates spacer
let table = Table::default().widths([Min(4), Min(4)]);
assert_eq!(table.get_column_widths(7, 0, 0), [(0, 3), (4, 3)]);
assert_eq!(
table.get_column_widths(7, 0, 0),
[Rect::new(0, 0, 3, 1), Rect::new(4, 0, 3, 1)]
);
// with selection, less than needed width
// always allocates selection and spacer
let table = Table::default().widths([Min(4), Min(4)]);
assert_eq!(table.get_column_widths(7, 3, 0), [(3, 2), (6, 1)]);
assert_eq!(
table.get_column_widths(7, 3, 0),
[Rect::new(3, 0, 2, 1), Rect::new(6, 0, 1, 1)]
);
}
#[test]
fn percentage_constraint() {
// without selection, more than needed width
let table = Table::default().widths([Percentage(30), Percentage(30)]);
assert_eq!(table.get_column_widths(20, 0, 0), [(0, 6), (7, 6)]);
assert_eq!(
table.get_column_widths(20, 0, 0),
[Rect::new(0, 0, 6, 1), Rect::new(7, 0, 6, 1)]
);
// with selection, more than needed width
let table = Table::default().widths([Percentage(30), Percentage(30)]);
assert_eq!(table.get_column_widths(20, 3, 0), [(3, 5), (9, 5)]);
assert_eq!(
table.get_column_widths(20, 3, 0),
[Rect::new(3, 0, 5, 1), Rect::new(9, 0, 5, 1)]
);
// without selection, less than needed width
// rounds from positions: [0.0, 0.0, 2.1, 3.1, 5.2, 7.0]
let table = Table::default().widths([Percentage(30), Percentage(30)]);
assert_eq!(table.get_column_widths(7, 0, 0), [(0, 2), (3, 2)]);
assert_eq!(
table.get_column_widths(7, 0, 0),
[Rect::new(0, 0, 2, 1), Rect::new(3, 0, 2, 1)]
);
// with selection, less than needed width
// rounds from positions: [0.0, 3.0, 5.1, 6.1, 7.0, 7.0]
let table = Table::default().widths([Percentage(30), Percentage(30)]);
assert_eq!(table.get_column_widths(7, 3, 0), [(3, 1), (5, 1)]);
assert_eq!(
table.get_column_widths(7, 3, 0),
[Rect::new(3, 0, 1, 1), Rect::new(5, 0, 1, 1)]
);
}
#[test]
@@ -1766,22 +1995,34 @@ mod tests {
// without selection, more than needed width
// rounds from positions: [0.00, 0.00, 6.67, 7.67, 14.33]
let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
assert_eq!(table.get_column_widths(20, 0, 0), [(0, 7), (8, 6)]);
assert_eq!(
table.get_column_widths(20, 0, 0),
[Rect::new(0, 0, 7, 1), Rect::new(8, 0, 6, 1)]
);
// with selection, more than needed width
// rounds from positions: [0.00, 3.00, 10.67, 17.33, 20.00]
let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
assert_eq!(table.get_column_widths(20, 3, 0), [(3, 6), (10, 5)]);
assert_eq!(
table.get_column_widths(20, 3, 0),
[Rect::new(3, 0, 6, 1), Rect::new(10, 0, 5, 1)]
);
// without selection, less than needed width
// rounds from positions: [0.00, 2.33, 3.33, 5.66, 7.00]
let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
assert_eq!(table.get_column_widths(7, 0, 0), [(0, 2), (3, 3)]);
assert_eq!(
table.get_column_widths(7, 0, 0),
[Rect::new(0, 0, 2, 1), Rect::new(3, 0, 3, 1)]
);
// with selection, less than needed width
// rounds from positions: [0.00, 3.00, 5.33, 6.33, 7.00, 7.00]
let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
assert_eq!(table.get_column_widths(7, 3, 0), [(3, 1), (5, 2)]);
assert_eq!(
table.get_column_widths(7, 3, 0),
[Rect::new(3, 0, 1, 1), Rect::new(5, 0, 2, 1)]
);
}
/// When more width is available than requested, the behavior is controlled by flex
@@ -1790,7 +2031,11 @@ mod tests {
let table = Table::default().widths([Min(10), Min(10), Min(1)]);
assert_eq!(
table.get_column_widths(62, 0, 0),
&[(0, 20), (21, 20), (42, 20)]
&[
Rect::new(0, 0, 20, 1),
Rect::new(21, 0, 20, 1),
Rect::new(42, 0, 20, 1)
]
);
let table = Table::default()
@@ -1798,7 +2043,11 @@ mod tests {
.flex(Flex::Legacy);
assert_eq!(
table.get_column_widths(62, 0, 0),
&[(0, 10), (11, 10), (22, 40)]
&[
Rect::new(0, 0, 10, 1),
Rect::new(11, 0, 10, 1),
Rect::new(22, 0, 40, 1)
]
);
let table = Table::default()
@@ -1806,7 +2055,11 @@ mod tests {
.flex(Flex::SpaceBetween);
assert_eq!(
table.get_column_widths(62, 0, 0),
&[(0, 20), (21, 20), (42, 20)]
&[
Rect::new(0, 0, 20, 1),
Rect::new(21, 0, 20, 1),
Rect::new(42, 0, 20, 1)
]
);
}
@@ -1815,7 +2068,11 @@ mod tests {
let table = Table::default().widths([Min(10), Min(10), Min(1)]);
assert_eq!(
table.get_column_widths(62, 0, 0),
&[(0, 20), (21, 20), (42, 20)]
&[
Rect::new(0, 0, 20, 1),
Rect::new(21, 0, 20, 1),
Rect::new(42, 0, 20, 1)
]
);
let table = Table::default()
@@ -1823,7 +2080,11 @@ mod tests {
.flex(Flex::Legacy);
assert_eq!(
table.get_column_widths(62, 0, 0),
&[(0, 10), (11, 10), (22, 40)]
&[
Rect::new(0, 0, 10, 1),
Rect::new(11, 0, 10, 1),
Rect::new(22, 0, 40, 1)
]
);
}
@@ -1840,7 +2101,11 @@ mod tests {
.column_spacing(0);
assert_eq!(
table.get_column_widths(30, 0, 3),
&[(0, 10), (10, 10), (20, 10)]
&[
Rect::new(0, 0, 10, 1),
Rect::new(10, 0, 10, 1),
Rect::new(20, 0, 10, 1)
]
);
}
@@ -1850,7 +2115,10 @@ mod tests {
.rows(vec![])
.header(Row::new(vec!["f", "g"]))
.column_spacing(0);
assert_eq!(table.get_column_widths(10, 0, 2), [(0, 5), (5, 5)]);
assert_eq!(
table.get_column_widths(10, 0, 2),
[Rect::new(0, 0, 5, 1), Rect::new(5, 0, 5, 1)]
);
}
#[test]
@@ -1859,7 +2127,10 @@ mod tests {
.rows(vec![])
.footer(Row::new(vec!["h", "i"]))
.column_spacing(0);
assert_eq!(table.get_column_widths(10, 0, 2), [(0, 5), (5, 5)]);
assert_eq!(
table.get_column_widths(10, 0, 2),
[Rect::new(0, 0, 5, 1), Rect::new(5, 0, 5, 1)]
);
}
#[track_caller]
@@ -2302,4 +2573,145 @@ mod tests {
// This should not panic, even if the buffer has zero size.
Widget::render(table, buffer.area, &mut buffer);
}
#[test]
fn get_area_for_column_span_one_no_more_columns() {
let columns = [];
let column_span = Table::get_cell_area(&mut columns.iter(), 1, 1);
assert!(column_span.is_none());
}
#[test]
fn get_area_for_column_span_two_no_more_columns() {
let columns = [];
let column_span = Table::get_cell_area(&mut columns.iter(), 2, 1);
assert!(column_span.is_none());
}
#[rstest]
#[case(&[Rect{x: 3, width: 2, y: 0, height: 1}, Rect{x: 3, width: 2, y: 0, height: 1}, Rect{x: 3, width: 2, y: 0, height: 1}], 2, 5)]
#[case(&[Rect{x: 3, width: 2, y: 0, height: 1}, Rect{x: 3, width: 2, y: 0, height: 1}], 2, 5,)]
#[case(&[Rect{x: 3, width: 2, y: 0, height: 1}, Rect{x: 3, width: 2, y: 0, height: 1}], 1, 2)]
#[case(&[Rect{x: 3, width: 2, y: 0, height: 1}, Rect{x: 3, width: 2, y: 0, height: 1}], 3, 5)]
#[case(&[Rect{x: 3, width: 2, y: 0, height: 1}], 1, 2)]
#[case(&[Rect{x: 3, width: 2, y: 0, height: 1}], 2, 2)]
#[case(&[
Rect{x: 3, width: 2, y: 0, height: 1},
Rect{x: 3, width: 2, y: 0, height: 1},
Rect{x: 3, width: 2, y: 0, height: 1},
Rect{x: 3, width: 2, y: 0, height: 1},
], 3, 8)]
#[case(&[
Rect{x: 3, width: 2, y: 0, height: 1},
Rect{x: 3, width: 2, y: 0, height: 1},
Rect{x: 3, width: 2, y: 0, height: 1},
], 3, 8)]
fn test_colspan_width_single_column_spacing(
#[case] columns: &[Rect],
#[case] column_span: u16,
#[case] expected_column_width: u16,
) {
let column_span = Table::get_cell_area(&mut columns.iter(), column_span, 1);
assert!(column_span.is_some());
assert_eq!(column_span.unwrap().width, expected_column_width);
}
#[rstest]
#[case(&[Rect{x: 3, width: 2, y: 0, height: 1}, Rect{x: 3, width: 2, y: 0, height: 1}, Rect{x: 3, width: 2, y: 0, height: 1}], 3, 10)]
#[case(&[Rect{x: 3, width: 2, y: 0, height: 1}], 3, 2)]
fn test_colspan_width_two_column_spacing(
#[case] columns: &[Rect],
#[case] column_span: u16,
#[case] expected_column_width: u16,
) {
let column_span = Table::get_cell_area(&mut columns.iter(), column_span, 2);
assert!(column_span.is_some());
assert_eq!(column_span.unwrap().width, expected_column_width);
}
#[rstest]
#[case(
HighlightSpacing::Always,
15, // width
1, // spacing
None, // selection
[
Cell::new("ABCDEFGHIJK").column_span(2),
Cell::new("12345678901"),
Cell::new("XYZXYZXYZXY"),
],
[
" ABCDEFGH 123",
" ", // row 2
" ", // row 3
])]
#[case(
HighlightSpacing::Always,
15, // width
1, // spacing
Some(0), // selection
[
Cell::new("ABCDEFGHIJK").column_span(2),
Cell::new("12345678901"),
Cell::new("XYZXYZXYZXY"),
],
[
">>>ABCDEFGH 123",
" ", // row 2
" ", // row 3
])]
#[case(
HighlightSpacing::WhenSelected,
15, // width
1, // spacing
None, // selection
[
Cell::new("ABCDEFGHIJK").column_span(2),
Cell::new("12345678901"),
Cell::new("XYZXYZXYZXY"),
],
[
"ABCDEFGHIJ 1234",
" ", // row 2
" ", // row 3
])]
#[case(
HighlightSpacing::WhenSelected,
15, // width
1, // spacing
Some(0), // selection
[
Cell::new("ABCDEFGHIJK").column_span(2),
Cell::new("12345678901"),
Cell::new("XYZXYZXYZXY"),
],
[
">>>ABCDEFGH 123",
" ", // row 2
" ", // row 3
])]
fn test_table_with_selection_and_column_spans<'line, 'cell, Lines, Cells>(
#[case] highlight_spacing: HighlightSpacing,
#[case] columns: u16,
#[case] spacing: u16,
#[case] selection: Option<usize>,
#[case] cells: Cells,
#[case] expected: Lines,
) where
Cells: IntoIterator,
Cells::Item: Into<Cell<'cell>>,
Lines: IntoIterator,
Lines::Item: Into<Line<'line>>,
{
let table = Table::default()
.rows(vec![Row::new(cells)])
.highlight_spacing(highlight_spacing)
.highlight_symbol(">>>")
.column_spacing(spacing);
let area = Rect::new(0, 0, columns, 3);
let mut buf = Buffer::empty(area);
let mut state = TableState::default().with_selected(selection);
StatefulWidget::render(table, area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(expected));
}
}

View File

@@ -51,6 +51,8 @@ use ratatui_core::widgets::Widget;
pub struct Cell<'a> {
content: Text<'a>,
style: Style,
/// The number of columns this cell will extend over
pub(crate) column_span: u16,
}
impl<'a> Cell<'a> {
@@ -80,6 +82,7 @@ impl<'a> Cell<'a> {
Self {
content: content.into(),
style: Style::default(),
column_span: 1,
}
}
@@ -113,6 +116,26 @@ impl<'a> Cell<'a> {
self
}
/// Set the `column_span` of this cell
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Example
/// ```rust
/// use ratatui::widgets::{Cell, Row};
/// let rows = vec![
/// Row::new(vec![Cell::new("12345").column_span(2)]),
/// Row::new(vec![Cell::new("xx"), Cell::new("yy")]),
/// ];
/// // "12345",
/// // "xx yy",
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn column_span(mut self, column_span: u16) -> Self {
self.column_span = column_span;
self
}
/// Set the `Style` of this cell
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
@@ -167,6 +190,7 @@ where
Self {
content: content.into(),
style: Style::default(),
column_span: 1,
}
}
}