Compare commits

..

1 Commits

Author SHA1 Message Date
Dheepak Krishnamurthy
9f1b8c9c68 feat: Adds message to assert_buffer_eq! 2024-01-18 23:38:33 -05:00
229 changed files with 17952 additions and 32424 deletions

16
.cargo-husky/hooks/pre-push Executable file
View File

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

View File

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

9
.github/CODEOWNERS vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,72 +17,76 @@ concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true cancel-in-progress: true
# lint, clippy and coverage jobs are intentionally early in the workflow to catch simple formatting, env:
# typos, and missing tests as early as possible. This allows us to fix these and resubmit the PR # don't install husky hooks during CI as they are only needed for for pre-push
# without having to wait for the comprehensive matrix of tests to complete. CARGO_HUSKY_DONT_INSTALL_HOOKS: true
# lint, clippy and coveraget jobs are intentionally early in the workflow to catch simple
# formatting, typos, and missing tests as early as possible. This allows us to fix these and
# resubmit the PR without having to wait for the comprehensive matrix of tests to complete.
jobs: jobs:
rustfmt: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - name: Checkout
- uses: dtolnay/rust-toolchain@nightly if: github.event_name != 'pull_request'
uses: actions/checkout@v4
- name: Checkout
if: github.event_name == 'pull_request'
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
with: with:
components: rustfmt components: rustfmt
- run: cargo +nightly fmt --all --check - name: Install cargo-make
uses: taiki-e/install-action@cargo-make
typos: - name: Check formatting
runs-on: ubuntu-latest run: cargo make lint-format
steps: - name: Check documentation
- uses: actions/checkout@v4 run: cargo make lint-docs
- uses: crate-ci/typos@master - name: Check conventional commits
uses: crate-ci/committed@master
dependencies: with:
runs-on: ubuntu-latest args: "-vv"
steps: commits: HEAD
- uses: actions/checkout@v4 - name: Check typos
- uses: EmbarkStudios/cargo-deny-action@v2 uses: crate-ci/typos@master
- name: Lint dependencies
cargo-machete: uses: EmbarkStudios/cargo-deny-action@v1
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- uses: bnjbvr/cargo-machete@v0.7.0
clippy: clippy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - name: Checkout
- uses: dtolnay/rust-toolchain@stable uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with: with:
components: clippy components: clippy
- uses: taiki-e/install-action@cargo-make - name: Install cargo-make
- uses: Swatinem/rust-cache@v2 uses: taiki-e/install-action@cargo-make
- run: cargo make clippy - name: Run cargo make clippy-all
run: cargo make clippy
markdownlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: DavidAnson/markdownlint-cli2-action@v17
with:
globs: |
'**/*.md'
'!target'
coverage: coverage:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - name: Checkout
- uses: dtolnay/rust-toolchain@stable uses: actions/checkout@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with: with:
components: llvm-tools components: llvm-tools
- uses: taiki-e/install-action@v2 - name: Install cargo-llvm-cov and cargo-make
uses: taiki-e/install-action@v2
with: with:
tool: cargo-llvm-cov,cargo-make tool: cargo-llvm-cov,cargo-make
- uses: Swatinem/rust-cache@v2 - name: Generate coverage
- run: cargo make coverage run: cargo make coverage
- uses: codecov/codecov-action@v4 - name: Upload to codecov.io
uses: codecov/codecov-action@v3
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true fail_ci_if_error: true
@@ -91,46 +95,38 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-latest] os: [ ubuntu-latest, windows-latest, macos-latest ]
toolchain: ["1.74.0", "stable"] toolchain: [ "1.70.0", "stable" ]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - name: Checkout
- uses: dtolnay/rust-toolchain@master uses: actions/checkout@v4
- name: Install Rust {{ matrix.toolchain }}
uses: dtolnay/rust-toolchain@master
with: with:
toolchain: ${{ matrix.toolchain }} toolchain: ${{ matrix.toolchain }}
- uses: taiki-e/install-action@cargo-make - name: Install cargo-make
- uses: Swatinem/rust-cache@v2 uses: taiki-e/install-action@cargo-make
- run: cargo make check - name: Run cargo make check
run: cargo make check
env: env:
RUST_BACKTRACE: full RUST_BACKTRACE: full
lint-docs:
runs-on: ubuntu-latest
env:
RUSTDOCFLAGS: -Dwarnings
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- uses: dtolnay/install@cargo-docs-rs
- uses: Swatinem/rust-cache@v2
# Run cargo rustdoc with the same options that would be used by docs.rs, taking into account
# the package.metadata.docs.rs configured in Cargo.toml.
# https://github.com/dtolnay/cargo-docs-rs
- run: cargo +nightly docs-rs
test-doc: test-doc:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-latest] os: [ ubuntu-latest, windows-latest, macos-latest ]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - name: Checkout
- uses: dtolnay/rust-toolchain@stable uses: actions/checkout@v4
- uses: taiki-e/install-action@cargo-make - name: Install Rust stable
- uses: Swatinem/rust-cache@v2 uses: dtolnay/rust-toolchain@stable
- run: cargo make test-doc - name: Install cargo-make
uses: taiki-e/install-action@cargo-make
- name: Test docs
run: cargo make test-doc
env: env:
RUST_BACKTRACE: full RUST_BACKTRACE: full
@@ -138,23 +134,26 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-latest] os: [ ubuntu-latest, windows-latest, macos-latest ]
toolchain: ["1.74.0", "stable"] toolchain: [ "1.70.0", "stable" ]
backend: [crossterm, termion, termwiz] backend: [ crossterm, termion, termwiz ]
exclude: exclude:
# termion is not supported on windows # termion is not supported on windows
- os: windows-latest - os: windows-latest
backend: termion backend: termion
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - name: Checkout
- uses: dtolnay/rust-toolchain@master uses: actions/checkout@v4
- name: Install Rust ${{ matrix.toolchain }}}
uses: dtolnay/rust-toolchain@master
with: with:
toolchain: ${{ matrix.toolchain }} toolchain: ${{ matrix.toolchain }}
- uses: taiki-e/install-action@v2 - name: Install cargo-make
with: uses: taiki-e/install-action@cargo-make
tool: cargo-make,nextest - name: Install cargo-nextest
- uses: Swatinem/rust-cache@v2 uses: taiki-e/install-action@nextest
- run: cargo make test-backend ${{ matrix.backend }} - name: Test ${{ matrix.backend }}
run: cargo make test-backend ${{ matrix.backend }}
env: env:
RUST_BACKTRACE: full RUST_BACKTRACE: full

View File

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

View File

@@ -2,40 +2,15 @@
This document contains a list of breaking changes in each version and some notes to help migrate This document contains a list of breaking changes in each version and some notes to help migrate
between versions. It is compiled manually from the commit history and changelog. We also tag PRs on between versions. It is compiled manually from the commit history and changelog. We also tag PRs on
GitHub with a [breaking change] label. github with a [breaking change] label.
[breaking change]: (https://github.com/ratatui/ratatui/issues?q=label%3A%22breaking+change%22) [breaking change]: (https://github.com/ratatui-org/ratatui/issues?q=label%3A%22breaking+change%22)
## Summary ## Summary
This is a quick summary of the sections below: This is a quick summary of the sections below:
- [v0.29.0](#unreleased) - [v0.26.0 (unreleased)](#v0260-unreleased)
- `Terminal`, `Buffer`, `Cell`, `Frame` are no longer `Sync` / `RefUnwindSafe`
- Removed public fields from `Rect` iterators
- `Line` now implements `From<Cow<str>`
- `Table::highlight_style` is now `Table::row_highlight_style`
- `Tabs::select` now accepts `Into<Option<usize>>`
- [v0.28.0](#v0280)
- `Backend::size` returns `Size` instead of `Rect`
- `Backend` trait migrates to `get/set_cursor_position`
- Ratatui now requires Crossterm 0.28.0
- `Axis::labels` now accepts `IntoIterator<Into<Line>>`
- `Layout::init_cache` no longer returns bool and takes a `NonZeroUsize` instead of `usize`
- `ratatui::terminal` module is now private
- `ToText` no longer has a lifetime
- `Frame::size` is deprecated and renamed to `Frame::area`
- [v0.27.0](#v0270)
- List no clamps the selected index to list
- Prelude items added / removed
- 'termion' updated to 4.0
- `Rect::inner` takes `Margin` directly instead of reference
- `Buffer::filled` takes `Cell` directly instead of reference
- `Stylize::bg()` now accepts `Into<Color>`
- Removed deprecated `List::start_corner`
- `LineGauge::gauge_style` is deprecated
- [v0.26.0](#v0260)
- `Flex::Start` is the new default flex mode for `Layout`
- `patch_style` & `reset_style` now consume and return `Self` - `patch_style` & `reset_style` now consume and return `Self`
- Removed deprecated `Block::title_on_bottom` - Removed deprecated `Block::title_on_bottom`
- `Line` now has an extra `style` field which applies the style to the entire line - `Line` now has an extra `style` field which applies the style to the entire line
@@ -61,7 +36,7 @@ This is a quick summary of the sections below:
- `Scrollbar`: symbols moved to `symbols` module - `Scrollbar`: symbols moved to `symbols` module
- MSRV is now 1.67.0 - MSRV is now 1.67.0
- [v0.22.0](#v0220) - [v0.22.0](#v0220)
- `serde` representation of `Borders` and `Modifiers` has changed - serde representation of `Borders` and `Modifiers` has changed
- [v0.21.0](#v0210) - [v0.21.0](#v0210)
- MSRV is now 1.65.0 - MSRV is now 1.65.0
- `terminal::ViewPort` is now an enum - `terminal::ViewPort` is now an enum
@@ -71,315 +46,13 @@ This is a quick summary of the sections below:
- MSRV is now 1.63.0 - MSRV is now 1.63.0
- `List` no longer ignores empty strings - `List` no longer ignores empty strings
## Unreleased ## v0.26.0 (unreleased)
### `Terminal`, `Buffer`, `Cell`, `Frame` are no longer `Sync` / `RefUnwindSafe` [#1339]
[#1339]: https://github.com/ratatui/ratatui/pull/1339
In #1339, we added a cache of the Cell width which uses a std::cell::Cell. This causes `Cell` and
all types that contain this (`Terminal`, `Buffer`, `Frame`, `CompletedFrame`, `TestBackend`) to no
longer be `Sync`
This change is unlikely to cause problems as these types likely should not be sent between threads
regardless due to their interaction with various things which mutated externally (e.g. stdio).
### Removed public fields from `Rect` iterators ([#1358])
[#1358]: https://github.com/ratatui/ratatui/pull/1358
The `pub` modifier has been removed from fields on the `layout::rect::Columns` and
`layout::rect::Rows`. These fields were not intended to be public and should not have been accessed
directly.
### `Rect::area()` now returns u32 instead of u16 ([#1378])
[#1378]: https://github.com/ratatui/ratatui/pull/1378
This is likely to impact anything which relies on `Rect::area` maxing out at u16::MAX. It can now
return up to u16::MAX * u16::MAX (2^32 - 2^17 + 1).
### `Line` now implements `From<Cow<str>` ([#1373])
[#1373]: https://github.com/ratatui/ratatui/pull/1373
As this adds an extra conversion, ambiguous inferred expressions may no longer compile.
```rust
// given:
struct Foo { ... }
impl From<Foo> for String { ... }
impl From<Foo> for Cow<str> { ... }
let foo = Foo { ... };
let line = Line::from(foo); // now fails due to now ambiguous inferred type
// replace with e.g.
let line = Line::from(String::from(foo));
```
### `Tabs::select()` now accepts `Into<Option<usize>>` ([#1413])
[#1413]: https://github.com/ratatui/ratatui/pull/1413
Previously `Tabs::select()` accepted `usize`, but it now accepts `Into<Option<usize>>`. This breaks
any code already using parameter type inference:
```diff
let selected = 1u8;
- let tabs = Tabs::new(["A", "B"]).select(selected.into())
+ let tabs = Tabs::new(["A", "B"]).select(selected as usize)
```
### `Table::highlight_style` is now `Table::row_highlight_style` ([#1331])
[#1331]: https://github.com/ratatui/ratatui/pull/1331
The `Table::highlight_style` is now deprecated in favor of `Table::row_highlight_style`.
Also, the serialized output of the `TableState` will now include the "selected_column" field.
Software that manually parse the serialized the output (with anything other than the `Serialize`
implementation on `TableState`) may have to be refactored if the "selected_column" field is not
accounted for. This does not affect users who rely on the `Deserialize`, or `Serialize`
implementation on the state.
## v0.28.0
### `Backend::size` returns `Size` instead of `Rect` ([#1254])
[#1254]: https://github.com/ratatui/ratatui/pull/1254
The `Backend::size` method returns a `Size` instead of a `Rect`.
There is no need for the position here as it was always 0,0.
### `Backend` trait migrates to `get/set_cursor_position` ([#1284])
[#1284]: https://github.com/ratatui/ratatui/pull/1284
If you just use the types implementing the `Backend` trait, you will see deprecation hints but
nothing is a breaking change for you.
If you implement the Backend trait yourself, you need to update the implementation and add the
`get/set_cursor_position` method. You can remove the `get/set_cursor` methods as they are deprecated
and a default implementation for them exists.
### Ratatui now requires Crossterm 0.28.0 ([#1278])
[#1278]: https://github.com/ratatui/ratatui/pull/1278
Crossterm is updated to version 0.28.0, which is a semver incompatible version with the previous
version (0.27.0). Ratatui re-exports the version of crossterm that it is compatible with under
`ratatui::crossterm`, which can be used to avoid incompatible versions in your dependency list.
### `Axis::labels()` now accepts `IntoIterator<Into<Line>>` ([#1273] and [#1283])
[#1273]: https://github.com/ratatui/ratatui/pull/1173
[#1283]: https://github.com/ratatui/ratatui/pull/1283
Previously Axis::labels accepted `Vec<Span>`. Any code that uses conversion methods that infer the
type will need to be rewritten as the compiler cannot infer the correct type.
```diff
- Axis::default().labels(vec!["a".into(), "b".into()])
+ Axis::default().labels(["a", "b"])
```
### `Layout::init_cache` no longer returns bool and takes a `NonZeroUsize` instead of `usize` ([#1245])
[#1245]: https://github.com/ratatui/ratatui/pull/1245
```diff
- let is_initialized = Layout::init_cache(100);
+ Layout::init_cache(NonZeroUsize::new(100).unwrap());
```
### `ratatui::terminal` module is now private ([#1160])
[#1160]: https://github.com/ratatui/ratatui/pull/1160
The `terminal` module is now private and can not be used directly. The types under this module are
exported from the root of the crate. This reduces clashes with other modules in the backends that
are also named terminal, and confusion about module exports for newer Rust users.
```diff
- use ratatui::terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
+ use ratatui::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
```
### `ToText` no longer has a lifetime ([#1234])
[#1234]: https://github.com/ratatui/ratatui/pull/1234
This change simplifies the trait and makes it easier to implement.
### `Frame::size` is deprecated and renamed to `Frame::area` ([#1293])
[#1293]: https://github.com/ratatui/ratatui/pull/1293
`Frame::size` is renamed to `Frame::area` as it's the more correct name.
## [v0.27.0](https://github.com/ratatui/ratatui/releases/tag/v0.27.0)
### List no clamps the selected index to list ([#1159])
[#1149]: https://github.com/ratatui/ratatui/pull/1149
The `List` widget now clamps the selected index to the bounds of the list when navigating with
`first`, `last`, `previous`, and `next`, as well as when setting the index directly with `select`.
Previously selecting an index past the end of the list would show treat the list as having a
selection which was not visible. Now the last item in the list will be selected instead.
### Prelude items added / removed ([#1149])
The following items have been removed from the prelude:
- `style::Styled` - this trait is useful for widgets that want to
support the Stylize trait, but it adds complexity as widgets have two
`style` methods and a `set_style` method.
- `symbols::Marker` - this item is used by code that needs to draw to
the `Canvas` widget, but it's not a common item that would be used by
most users of the library.
- `terminal::{CompletedFrame, TerminalOptions, Viewport}` - these items
are rarely used by code that needs to interact with the terminal, and
they're generally only ever used once in any app.
The following items have been added to the prelude:
- `layout::{Position, Size}` - these items are used by code that needs
to interact with the layout system. These are newer items that were
added in the last few releases, which should be used more liberally.
This may cause conflicts for types defined elsewhere with a similar
name.
To update your app:
```diff
// if your app uses Styled::style() or Styled::set_style():
-use ratatui::prelude::*;
+use ratatui::{prelude::*, style::Styled};
// if your app uses symbols::Marker:
-use ratatui::prelude::*;
+use ratatui::{prelude::*, symbols::Marker}
// if your app uses terminal::{CompletedFrame, TerminalOptions, Viewport}
-use ratatui::prelude::*;
+use ratatui::{prelude::*, terminal::{CompletedFrame, TerminalOptions, Viewport}};
// to disambiguate existing types named Position or Size:
- use some_crate::{Position, Size};
- let size: Size = ...;
- let position: Position = ...;
+ let size: some_crate::Size = ...;
+ let position: some_crate::Position = ...;
```
### Termion is updated to 4.0 [#1106]
Changelog: <https://gitlab.redox-os.org/redox-os/termion/-/blob/master/CHANGELOG.md>
A change is only necessary if you were matching on all variants of the `MouseEvent` enum without a
wildcard. In this case, you need to either handle the two new variants, `MouseLeft` and
`MouseRight`, or add a wildcard.
[#1106]: https://github.com/ratatui/ratatui/pull/1106
### `Rect::inner` takes `Margin` directly instead of reference ([#1008])
[#1008]: https://github.com/ratatui/ratatui/pull/1008
`Margin` needs to be passed without reference now.
```diff
-let area = area.inner(&Margin {
+let area = area.inner(Margin {
vertical: 0,
horizontal: 2,
});
```
### `Buffer::filled` takes `Cell` directly instead of reference ([#1148])
[#1148]: https://github.com/ratatui/ratatui/pull/1148
`Buffer::filled` moves the `Cell` instead of taking a reference.
```diff
-Buffer::filled(area, &Cell::new("X"));
+Buffer::filled(area, Cell::new("X"));
```
### `Stylize::bg()` now accepts `Into<Color>` ([#1103])
[#1103]: https://github.com/ratatui/ratatui/pull/1103
Previously, `Stylize::bg()` accepted `Color` but now accepts `Into<Color>`. This allows more
flexible types from calling scopes, though it can break some type inference in the calling scope.
### Remove deprecated `List::start_corner` and `layout::Corner` ([#759])
[#759]: https://github.com/ratatui/ratatui/pull/759
`List::start_corner` was deprecated in v0.25. Use `List::direction` and `ListDirection` instead.
```diff
- list.start_corner(Corner::TopLeft);
- list.start_corner(Corner::TopRight);
// This is not an error, BottomRight rendered top to bottom previously
- list.start_corner(Corner::BottomRight);
// all becomes
+ list.direction(ListDirection::TopToBottom);
```
```diff
- list.start_corner(Corner::BottomLeft);
// becomes
+ list.direction(ListDirection::BottomToTop);
```
`layout::Corner` was removed entirely.
### `LineGauge::gauge_style` is deprecated ([#565])
[#565]: https://github.com/ratatui/ratatui/pull/1148
`LineGauge::gauge_style` is deprecated and replaced with `LineGauge::filled_style` and `LineGauge::unfilled_style`:
```diff
let gauge = LineGauge::default()
- .gauge_style(Style::default().fg(Color::Red).bg(Color::Blue)
+ .filled_style(Style::default().fg(Color::Green))
+ .unfilled_style(Style::default().fg(Color::White));
```
## [v0.26.0](https://github.com/ratatui/ratatui/releases/tag/v0.26.0)
### `Flex::Start` is the new default flex mode for `Layout` ([#881])
[#881]: https://github.com/ratatui/ratatui/pull/881
Previously, constraints would stretch to fill all available space, violating constraints if
necessary.
With v0.26.0, `Flex` modes are introduced, and the default is `Flex::Start`, which will align
areas associated with constraints to be beginning of the area. With v0.26.0, additionally,
`Min` constraints grow to fill excess space. These changes will allow users to build layouts
more easily.
With v0.26.0, users will most likely not need to change what constraints they use to create
existing layouts with `Flex::Start`. However, to get old behavior, use `Flex::Legacy`.
```diff
- let rects = Layout::horizontal([Length(1), Length(2)]).split(area);
// becomes
+ let rects = Layout::horizontal([Length(1), Length(2)]).flex(Flex::Legacy).split(area);
```
### `Table::new()` now accepts `IntoIterator<Item: Into<Row<'a>>>` ([#774]) ### `Table::new()` now accepts `IntoIterator<Item: Into<Row<'a>>>` ([#774])
[#774]: https://github.com/ratatui/ratatui/pull/774 [#774]: https://github.com/ratatui-org/ratatui/pull/774
Previously, `Table::new()` accepted `IntoIterator<Item=Row<'a>>`. The argument change to Previously, `Table::new()` accepted `IntoIterator<Item=Row<'a>>`. The argument change to
`IntoIterator<Item: Into<Row<'a>>>`, This allows more flexible types from calling scopes, though it `IntoIterator<Item: Into<Row<'a>>>`, This allows more flexible types from calling scopes, though it
can some break type inference in the calling scope for empty containers. can some break type inference in the calling scope for empty containers.
@@ -394,9 +67,9 @@ This can be resolved either by providing an explicit type (e.g. `Vec::<Row>::new
### `Tabs::new()` now accepts `IntoIterator<Item: Into<Line<'a>>>` ([#776]) ### `Tabs::new()` now accepts `IntoIterator<Item: Into<Line<'a>>>` ([#776])
[#776]: https://github.com/ratatui/ratatui/pull/776 [#776]: https://github.com/ratatui-org/ratatui/pull/776
Previously, `Tabs::new()` accepted `Vec<T>` where `T: Into<Line<'a>>`. This allows more flexible Previously, `Tabs::new()` accepted `Vec<T>` where `T: Into<Line<'a>>`. This allows more flexible
types from calling scopes, though it can break some type inference in the calling scope. types from calling scopes, though it can break some type inference in the calling scope.
This typically occurs when collecting an iterator prior to calling `Tabs::new`, and can be resolved This typically occurs when collecting an iterator prior to calling `Tabs::new`, and can be resolved
@@ -410,17 +83,17 @@ by removing the call to `.collect()`.
### Table::default() now sets segment_size to None and column_spacing to ([#751]) ### Table::default() now sets segment_size to None and column_spacing to ([#751])
[#751]: https://github.com/ratatui/ratatui/pull/751 [#751]: https://github.com/ratatui-org/ratatui/pull/751
The default() implementation of Table now sets the column_spacing field to 1 and the segment_size The default() implementation of Table now sets the column_spacing field to 1 and the segment_size
field to `SegmentSize::None`. This will affect the rendering of a small amount of apps. field to SegmentSize::None. This will affect the rendering of a small amount of apps.
To use the previous default values, call `table.segment_size(Default::default())` and To use the previous default values, call `table.segment_size(Default::default())` and
`table.column_spacing(0)`. `table.column_spacing(0)`.
### `patch_style` & `reset_style` now consumes and returns `Self` ([#754]) ### `patch_style` & `reset_style` now consumes and returns `Self` ([#754])
[#754]: https://github.com/ratatui/ratatui/pull/754 [#754]: https://github.com/ratatui-org/ratatui/pull/754
Previously, `patch_style` and `reset_style` in `Text`, `Line` and `Span` were using a mutable Previously, `patch_style` and `reset_style` in `Text`, `Line` and `Span` were using a mutable
reference to `Self`. To be more consistent with the rest of `ratatui`, which is using fluent reference to `Self`. To be more consistent with the rest of `ratatui`, which is using fluent
@@ -437,6 +110,8 @@ The following example shows how to migrate for `Line`, but the same applies for
### Remove deprecated `Block::title_on_bottom` ([#757]) ### Remove deprecated `Block::title_on_bottom` ([#757])
[#757]: https://github.com/ratatui-org/ratatui/pull/757
`Block::title_on_bottom` was deprecated in v0.22. Use `Block::title` and `Title::position` instead. `Block::title_on_bottom` was deprecated in v0.22. Use `Block::title` and `Title::position` instead.
```diff ```diff
@@ -446,7 +121,7 @@ The following example shows how to migrate for `Line`, but the same applies for
### `Block` style methods cannot be used in a const context ([#720]) ### `Block` style methods cannot be used in a const context ([#720])
[#720]: https://github.com/ratatui/ratatui/pull/720 [#720]: https://github.com/ratatui-org/ratatui/pull/720
Previously the `style()`, `border_style()` and `title_style()` methods could be used to create a Previously the `style()`, `border_style()` and `title_style()` methods could be used to create a
`Block` in a constant context. These now accept `Into<Style>` instead of `Style`. These methods no `Block` in a constant context. These now accept `Into<Style>` instead of `Style`. These methods no
@@ -454,10 +129,10 @@ longer can be called from a constant context.
### `Line` now has a `style` field that applies to the entire line ([#708]) ### `Line` now has a `style` field that applies to the entire line ([#708])
[#708]: https://github.com/ratatui/ratatui/pull/708 [#708]: https://github.com/ratatui-org/ratatui/pull/708
Previously the style of a `Line` was stored in the `Span`s that make up the line. Now the `Line` Previously the style of a `Line` was stored in the `Span`s that make up the line. Now the `Line`
itself has a `style` field, which can be set with the `Line::styled` method. Any code that creates itself has a `style` field, which can be set with the `Line::style` method. Any code that creates
`Line`s using the struct initializer instead of constructors will fail to compile due to the added `Line`s using the struct initializer instead of constructors will fail to compile due to the added
field. This can be easily fixed by adding `..Default::default()` to the field list or by using a field. This can be easily fixed by adding `..Default::default()` to the field list or by using a
constructor method (`Line::styled()`, `Line::raw()`) or conversion method (`Line::from()`). constructor method (`Line::styled()`, `Line::raw()`) or conversion method (`Line::from()`).
@@ -478,11 +153,11 @@ the `Span::style` field.
.alignment(Alignment::Left); .alignment(Alignment::Left);
``` ```
## [v0.25.0](https://github.com/ratatui/ratatui/releases/tag/v0.25.0) ## [v0.25.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.25.0)
### Removed `Axis::title_style` and `Buffer::set_background` ([#691]) ### Removed `Axis::title_style` and `Buffer::set_background` ([#691])
[#691]: https://github.com/ratatui/ratatui/pull/691 [#691]: https://github.com/ratatui-org/ratatui/pull/691
These items were deprecated since 0.10. These items were deprecated since 0.10.
@@ -496,7 +171,7 @@ These items were deprecated since 0.10.
### `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>` ([#672]) ### `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>` ([#672])
[#672]: https://github.com/ratatui/ratatui/pull/672 [#672]: https://github.com/ratatui-org/ratatui/pull/672
Previously `List::new()` took `Into<Vec<ListItem<'a>>>`. This change will throw a compilation Previously `List::new()` took `Into<Vec<ListItem<'a>>>`. This change will throw a compilation
error for `IntoIterator`s with an indeterminate item (e.g. empty vecs). error for `IntoIterator`s with an indeterminate item (e.g. empty vecs).
@@ -511,7 +186,7 @@ E.g.
### The default `Tabs::highlight_style` is now `Style::new().reversed()` ([#635]) ### The default `Tabs::highlight_style` is now `Style::new().reversed()` ([#635])
[#635]: https://github.com/ratatui/ratatui/pull/635 [#635]: https://github.com/ratatui-org/ratatui/pull/635
Previously the default highlight style for tabs was `Style::default()`, which meant that a `Tabs` Previously the default highlight style for tabs was `Style::default()`, which meant that a `Tabs`
widget in the default configuration would not show any indication of the selected tab. widget in the default configuration would not show any indication of the selected tab.
@@ -523,10 +198,10 @@ widget in the default configuration would not show any indication of the selecte
### `Table::new()` now requires specifying the widths of the columns ([#664]) ### `Table::new()` now requires specifying the widths of the columns ([#664])
[#664]: https://github.com/ratatui/ratatui/pull/664 [#664]: https://github.com/ratatui-org/ratatui/pull/664
Previously `Table`s could be constructed without `widths`. In almost all cases this is an error. Previously `Table`s could be constructed without widths. In almost all cases this is an error.
A new `widths` parameter is now mandatory on `Table::new()`. Existing code of the form: A new widths parameter is now mandatory on `Table::new()`. Existing code of the form:
```diff ```diff
- Table::new(rows).widths(widths) - Table::new(rows).widths(widths)
@@ -549,7 +224,7 @@ or complex, it may be convenient to replace `Table::new` with `Table::default().
### `Table::widths()` now accepts `IntoIterator<Item = AsRef<Constraint>>` ([#663]) ### `Table::widths()` now accepts `IntoIterator<Item = AsRef<Constraint>>` ([#663])
[#663]: https://github.com/ratatui/ratatui/pull/663 [#663]: https://github.com/ratatui-org/ratatui/pull/663
Previously `Table::widths()` took a slice (`&'a [Constraint]`). This change will introduce clippy Previously `Table::widths()` took a slice (`&'a [Constraint]`). This change will introduce clippy
`needless_borrow` warnings for places where slices are passed to this method. To fix these, remove `needless_borrow` warnings for places where slices are passed to this method. To fix these, remove
@@ -565,7 +240,7 @@ E.g.
### Layout::new() now accepts direction and constraint parameters ([#557]) ### Layout::new() now accepts direction and constraint parameters ([#557])
[#557]: https://github.com/ratatui/ratatui/pull/557 [#557]: https://github.com/ratatui-org/ratatui/pull/557
Previously layout new took no parameters. Existing code should either use `Layout::default()` or Previously layout new took no parameters. Existing code should either use `Layout::default()` or
the new constructor. the new constructor.
@@ -582,18 +257,18 @@ let layout = layout::default()
let layout = layout::new(Direction::Vertical, [Constraint::Min(1), Constraint::Max(2)]); let layout = layout::new(Direction::Vertical, [Constraint::Min(1), Constraint::Max(2)]);
``` ```
## [v0.24.0](https://github.com/ratatui/ratatui/releases/tag/v0.24.0) ## [v0.24.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.24.0)
### `ScrollbarState` field type changed from `u16` to `usize` ([#456]) ### ScrollbarState field type changed from `u16` to `usize` ([#456])
[#456]: https://github.com/ratatui/ratatui/pull/456 [#456]: https://github.com/ratatui-org/ratatui/pull/456
In order to support larger content lengths, the `position`, `content_length` and In order to support larger content lengths, the `position`, `content_length` and
`viewport_content_length` methods on `ScrollbarState` now take `usize` instead of `u16` `viewport_content_length` methods on `ScrollbarState` now take `usize` instead of `u16`
### `BorderType::line_symbols` renamed to `border_symbols` ([#529]) ### `BorderType::line_symbols` renamed to `border_symbols` ([#529])
[#529]: https://github.com/ratatui/ratatui/issues/529 [#529]: https://github.com/ratatui-org/ratatui/issues/529
Applications can now set custom borders on a `Block` by calling `border_set()`. The Applications can now set custom borders on a `Block` by calling `border_set()`. The
`BorderType::line_symbols()` is renamed to `border_symbols()` and now returns a new struct `BorderType::line_symbols()` is renamed to `border_symbols()` and now returns a new struct
@@ -607,7 +282,7 @@ Applications can now set custom borders on a `Block` by calling `border_set()`.
### Generic `Backend` parameter removed from `Frame` ([#530]) ### Generic `Backend` parameter removed from `Frame` ([#530])
[#530]: https://github.com/ratatui/ratatui/issues/530 [#530]: https://github.com/ratatui-org/ratatui/issues/530
`Frame` is no longer generic over Backend. Code that accepted `Frame<Backend>` will now need to `Frame` is no longer generic over Backend. Code that accepted `Frame<Backend>` will now need to
accept `Frame`. To migrate existing code, remove any generic parameters from code that uses an accept `Frame`. To migrate existing code, remove any generic parameters from code that uses an
@@ -621,7 +296,7 @@ instance of a Frame. E.g.:
### `Stylize` shorthands now consume rather than borrow `String` ([#466]) ### `Stylize` shorthands now consume rather than borrow `String` ([#466])
[#466]: https://github.com/ratatui/ratatui/issues/466 [#466]: https://github.com/ratatui-org/ratatui/issues/466
In order to support using `Stylize` shorthands (e.g. `"foo".red()`) on temporary `String` values, a In order to support using `Stylize` shorthands (e.g. `"foo".red()`) on temporary `String` values, a
new implementation of `Stylize` was added that returns a `Span<'static>`. This causes the value to new implementation of `Stylize` was added that returns a `Span<'static>`. This causes the value to
@@ -639,7 +314,7 @@ longer compile. E.g.
### Deprecated `Spans` type removed (replaced with `Line`) ([#426]) ### Deprecated `Spans` type removed (replaced with `Line`) ([#426])
[#426]: https://github.com/ratatui/ratatui/issues/426 [#426]: https://github.com/ratatui-org/ratatui/issues/426
`Spans` was replaced with `Line` in 0.21.0. `Buffer::set_spans` was replaced with `Spans` was replaced with `Line` in 0.21.0. `Buffer::set_spans` was replaced with
`Buffer::set_line`. `Buffer::set_line`.
@@ -652,11 +327,11 @@ longer compile. E.g.
+ buffer.set_line(0, 0, line, 10); + buffer.set_line(0, 0, line, 10);
``` ```
## [v0.23.0](https://github.com/ratatui/ratatui/releases/tag/v0.23.0) ## [v0.23.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.23.0)
### `Scrollbar::track_symbol()` now takes an `Option<&str>` instead of `&str` ([#360]) ### `Scrollbar::track_symbol()` now takes an `Option<&str>` instead of `&str` ([#360])
[#360]: https://github.com/ratatui/ratatui/issues/360 [#360]: https://github.com/ratatui-org/ratatui/issues/360
The track symbol of `Scrollbar` is now optional, this method now takes an optional value. The track symbol of `Scrollbar` is now optional, this method now takes an optional value.
@@ -668,7 +343,7 @@ The track symbol of `Scrollbar` is now optional, this method now takes an option
### `Scrollbar` symbols moved to `symbols::scrollbar` and `widgets::scrollbar` module is private ([#330]) ### `Scrollbar` symbols moved to `symbols::scrollbar` and `widgets::scrollbar` module is private ([#330])
[#330]: https://github.com/ratatui/ratatui/issues/330 [#330]: https://github.com/ratatui-org/ratatui/issues/330
The symbols for defining scrollbars have been moved to the `symbols` module from the The symbols for defining scrollbars have been moved to the `symbols` module from the
`widgets::scrollbar` module which is no longer public. To update your code update any imports to the `widgets::scrollbar` module which is no longer public. To update your code update any imports to the
@@ -682,31 +357,31 @@ new module locations. E.g.:
### MSRV updated to 1.67 ([#361]) ### MSRV updated to 1.67 ([#361])
[#361]: https://github.com/ratatui/ratatui/issues/361 [#361]: https://github.com/ratatui-org/ratatui/issues/361
The MSRV of ratatui is now 1.67 due to an MSRV update in a dependency (`time`). The MSRV of ratatui is now 1.67 due to an MSRV update in a dependency (`time`).
## [v0.22.0](https://github.com/ratatui/ratatui/releases/tag/v0.22.0) ## [v0.22.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.22.0)
### `bitflags` updated to 2.3 ([#205]) ### bitflags updated to 2.3 ([#205])
[#205]: https://github.com/ratatui/ratatui/issues/205 [#205]: https://github.com/ratatui-org/ratatui/issues/205
The `serde` representation of `bitflags` has changed. Any existing serialized types that have The serde representation of bitflags has changed. Any existing serialized types that have Borders or
Borders or Modifiers will need to be re-serialized. This is documented in the [`bitflags` Modifiers will need to be re-serialized. This is documented in the [bitflags
changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md#200-rc2).. changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md#200-rc2)..
## [v0.21.0](https://github.com/ratatui/ratatui/releases/tag/v0.21.0) ## [v0.21.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.21.0)
### MSRV is 1.65.0 ([#171]) ### MSRV is 1.65.0 ([#171])
[#171]: https://github.com/ratatui/ratatui/issues/171 [#171]: https://github.com/ratatui-org/ratatui/issues/171
The minimum supported rust version is now 1.65.0. The minimum supported rust version is now 1.65.0.
### `Terminal::with_options()` stabilized to allow configuring the viewport ([#114]) ### `Terminal::with_options()` stabilized to allow configuring the viewport ([#114])
[#114]: https://github.com/ratatui/ratatui/issues/114 [#114]: https://github.com/ratatui-org/ratatui/issues/114
In order to support inline viewports, the unstable method `Terminal::with_options()` was stabilized In order to support inline viewports, the unstable method `Terminal::with_options()` was stabilized
and `ViewPort` was changed from a struct to an enum. and `ViewPort` was changed from a struct to an enum.
@@ -723,11 +398,11 @@ let terminal = Terminal::with_options(backend, TerminalOptions {
### Code that binds `Into<Text<'a>>` now requires type annotations ([#168]) ### Code that binds `Into<Text<'a>>` now requires type annotations ([#168])
[#168]: https://github.com/ratatui/ratatui/issues/168 [#168]: https://github.com/ratatui-org/ratatui/issues/168
A new type `Masked` was introduced that implements `From<Text<'a>>`. This causes any code that A new type `Masked` was introduced that implements `From<Text<'a>>`. This causes any code that did
previously did not need to use type annotations to fail to compile. To fix this, annotate or call previously did not need to use type annotations to fail to compile. To fix this, annotate or call
`to_string()` / `to_owned()` / `as_str()` on the value. E.g.: to_string() / to_owned() / as_str() on the value. E.g.:
```diff ```diff
- let paragraph = Paragraph::new("".as_ref()); - let paragraph = Paragraph::new("".as_ref());
@@ -737,7 +412,7 @@ previously did not need to use type annotations to fail to compile. To fix this,
### `Marker::Block` now renders as a block rather than a bar character ([#133]) ### `Marker::Block` now renders as a block rather than a bar character ([#133])
[#133]: https://github.com/ratatui/ratatui/issues/133 [#133]: https://github.com/ratatui-org/ratatui/issues/133
Code using the `Block` marker that previously rendered using a half block character (`'▀'``) now Code using the `Block` marker that previously rendered using a half block character (`'▀'``) now
renders using the full block character (`'█'`). A new marker variant`Bar` is introduced to replace renders using the full block character (`'█'`). A new marker variant`Bar` is introduced to replace
@@ -749,20 +424,20 @@ the existing code.
+ let canvas = Canvas::default().marker(Marker::Bar); + let canvas = Canvas::default().marker(Marker::Bar);
``` ```
## [v0.20.0](https://github.com/ratatui/ratatui/releases/tag/v0.20.0) ## [v0.20.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.20.0)
v0.20.0 was the first release of Ratatui - versions prior to this were release as tui-rs. See the v0.20.0 was the first release of Ratatui - versions prior to this were release as tui-rs. See the
[Changelog](./CHANGELOG.md) for more details. [Changelog](./CHANGELOG.md) for more details.
### MSRV is update to 1.63.0 ([#80]) ### MSRV is update to 1.63.0 ([#80])
[#80]: https://github.com/ratatui/ratatui/issues/80 [#80]: https://github.com/ratatui-org/ratatui/issues/80
The minimum supported rust version is 1.63.0 The minimum supported rust version is 1.63.0
### List no longer ignores empty string in items ([#42]) ### List no longer ignores empty string in items ([#42])
[#42]: https://github.com/ratatui/ratatui/issues/42 [#42]: https://github.com/ratatui-org/ratatui/issues/42
The following code now renders 3 items instead of 2. Code which relies on the previous behavior will The following code now renders 3 items instead of 2. Code which relies on the previous behavior will
need to manually filter empty items prior to display. need to manually filter empty items prior to display.

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -8,7 +8,7 @@ creating a new issue before making the change, or starting a discussion on
## Reporting issues ## Reporting issues
Before reporting an issue on the [issue tracker](https://github.com/ratatui/ratatui/issues), Before reporting an issue on the [issue tracker](https://github.com/ratatui-org/ratatui/issues),
please check that it has not already been reported by searching for some related keywords. Please please check that it has not already been reported by searching for some related keywords. Please
also check [`tui-rs` issues](https://github.com/fdehau/tui-rs/issues/) and link any related issues also check [`tui-rs` issues](https://github.com/fdehau/tui-rs/issues/) and link any related issues
found. found.
@@ -17,10 +17,10 @@ found.
All contributions are obviously welcome. Please include as many details as possible in your PR All contributions are obviously welcome. Please include as many details as possible in your PR
description to help the reviewer (follow the provided template). Make sure to highlight changes description to help the reviewer (follow the provided template). Make sure to highlight changes
which may need additional attention, or you are uncertain about. Any idea with a large scale impact which may need additional attention or you are uncertain about. Any idea with a large scale impact
on the crate or its users should ideally be discussed in a "Feature Request" issue beforehand. on the crate or its users should ideally be discussed in a "Feature Request" issue beforehand.
### Keep PRs small, intentional, and focused ### Keep PRs small, intentional and focused
Try to do one pull request per change. The time taken to review a PR grows exponential with the size Try to do one pull request per change. The time taken to review a PR grows exponential with the size
of the change. Small focused PRs will generally be much more faster to review. PRs that include both of the change. Small focused PRs will generally be much more faster to review. PRs that include both
@@ -32,7 +32,7 @@ guarantee that the behavior is unchanged.
### Code formatting ### Code formatting
Run `cargo make format` before committing to ensure that code is consistently formatted with Run `cargo make format` before committing to ensure that code is consistently formatted with
rustfmt. Configuration is in [`rustfmt.toml`](./rustfmt.toml). rustfmt. Configuration is in [rustfmt.toml](./rustfmt.toml).
### Search `tui-rs` for similar work ### Search `tui-rs` for similar work
@@ -54,11 +54,21 @@ describes the nature of the problem that the commit is solving and any unintuiti
change. It's rare that code changes can easily communicate intent, so make sure this is clearly change. It's rare that code changes can easily communicate intent, so make sure this is clearly
documented. documented.
### Clean up your commits
The final version of your PR that will be committed to the repository should be rebased and tested
against main. Every commit will end up as a line in the changelog, so please squash commits that are
only formatting or incremental fixes to things brought up as part of the PR review. Aim for a single
commit (unless there is a strong reason to stack the commits). See [Git Best Practices - On Sausage
Making](https://sethrobertson.github.io/GitBestPractices/#sausage) for more on this.
### Run CI tests before pushing a PR ### Run CI tests before pushing a PR
Running `cargo make ci` before pushing will perform the same checks that we do in the CI process. We're using [cargo-husky](https://github.com/rhysd/cargo-husky) to automatically run git hooks,
It's not mandatory to do this before pushing, however it may save you time to do so instead of which will run `cargo make ci` before each push. To initialize the hook run `cargo test`. If
waiting for GitHub to run the checks. `cargo-make` is not installed, it will provide instructions to install it for you. This will ensure
that your code is formatted, compiles and passes all tests before you push. If you need to skip this
check, you can use `git push --no-verify`.
### Sign your commits ### Sign your commits
@@ -79,14 +89,14 @@ defaults depending on your platform of choice. Building the project should be as
`cargo make build`. `cargo make build`.
```shell ```shell
git clone https://github.com/ratatui/ratatui.git git clone https://github.com/ratatui-org/ratatui.git
cd ratatui cd ratatui
cargo make build cargo make build
``` ```
### Tests ### Tests
The [test coverage](https://app.codecov.io/gh/ratatui/ratatui) of the crate is reasonably The [test coverage](https://app.codecov.io/gh/ratatui-org/ratatui) of the crate is reasonably
good, but this can always be improved. Focus on keeping the tests simple and obvious and write unit good, but this can always be improved. Focus on keeping the tests simple and obvious and write unit
tests for all new or modified code. Beside the usual doc and unit tests, one of the most valuable tests for all new or modified code. Beside the usual doc and unit tests, one of the most valuable
test you can write for Ratatui is a test against the `TestBackend`. It allows you to assert the test you can write for Ratatui is a test against the `TestBackend`. It allows you to assert the
@@ -110,8 +120,7 @@ exist to show coverage directly in your editor. E.g.:
### Documentation ### Documentation
Here are some guidelines for writing documentation in Ratatui. Here are some guidelines for writing documentation in Ratatui.
Every public API **must** be documented. Every public API **must** be documented.
Keep in mind that Ratatui tends to attract beginner Rust users that may not be familiar with Rust Keep in mind that Ratatui tends to attract beginner Rust users that may not be familiar with Rust
@@ -124,9 +133,10 @@ the concepts pointing to the various methods. Focus on interaction with various
enough information that helps understand why you might want something. enough information that helps understand why you might want something.
Examples should help users understand a particular usage, not test a feature. They should be as Examples should help users understand a particular usage, not test a feature. They should be as
simple as possible. Prefer hiding imports and using wildcards to keep things concise. Some imports simple as possible.
may still be shown to demonstrate a particular non-obvious import (e.g. `Stylize` trait to use style Prefer hiding imports and using wildcards to keep things concise. Some imports may still be shown
methods). Speaking of `Stylize`, you should use it over the more verbose style setters: to demonstrate a particular non-obvious import (e.g. `Stylize` trait to use style methods).
Speaking of `Stylize`, you should use it over the more verbose style setters:
```rust ```rust
let style = Style::new().red().bold(); let style = Style::new().red().bold();
@@ -136,7 +146,7 @@ let style = Style::default().fg(Color::Red).add_modifier(Modifiers::BOLD);
#### Format #### Format
- First line is summary, second is blank, third onward is more detail - First line is summary, second is blank, third onward is more detail
```rust ```rust
/// Summary /// Summary
@@ -146,10 +156,10 @@ let style = Style::default().fg(Color::Red).add_modifier(Modifiers::BOLD);
fn foo() {} fn foo() {}
``` ```
- Max line length is 100 characters - Max line length is 100 characters
See [VS Code rewrap extension](https://marketplace.visualstudio.com/items?itemName=stkb.rewrap) See [vscode rewrap extension](https://marketplace.visualstudio.com/items?itemName=stkb.rewrap)
- Doc comments are above macros - Doc comments are above macros
i.e. i.e.
```rust ```rust
@@ -158,24 +168,24 @@ i.e.
struct Foo {} struct Foo {}
``` ```
- Code items should be between backticks - Code items should be between backticks
i.e. ``[`Block`]``, **NOT** ``[Block]`` i.e. ``[`Block`]``, **NOT** ``[Block]``
### Deprecation notice ### Deprecation notice
We generally want to wait at least two versions before removing deprecated items, so users have We generally want to wait at least two versions before removing deprecated items so users have
time to update. However, if a deprecation is blocking for us to implement a new feature we may time to update. However, if a deprecation is blocking for us to implement a new feature we may
*consider* removing it in a one version notice. *consider* removing it in a one version notice.
### Use of unsafe for optimization purposes ### Use of unsafe for optimization purposes
We don't currently use any unsafe code in Ratatui, and would like to keep it that way. However, there We don't currently use any unsafe code in Ratatui, and would like to keep it that way. However there
may be specific cases that this becomes necessary in order to avoid slowness. Please see [this may be specific cases that this becomes necessary in order to avoid slowness. Please see [this
discussion](https://github.com/ratatui/ratatui/discussions/66) for more about the decision. discussion](https://github.com/ratatui-org/ratatui/discussions/66) for more about the decision.
## Continuous Integration ## Continuous Integration
We use GitHub Actions for the CI where we perform the following checks: We use Github Actions for the CI where we perform the following checks:
- The code should compile on `stable` and the Minimum Supported Rust Version (MSRV). - The code should compile on `stable` and the Minimum Supported Rust Version (MSRV).
- The tests (docs, lib, tests and examples) should pass. - The tests (docs, lib, tests and examples) should pass.
@@ -191,12 +201,12 @@ This project was forked from [`tui-rs`](https://github.com/fdehau/tui-rs/) in Fe
[blessing of the original author](https://github.com/fdehau/tui-rs/issues/654), Florian Dehau [blessing of the original author](https://github.com/fdehau/tui-rs/issues/654), Florian Dehau
([@fdehau](https://github.com/fdehau)). ([@fdehau](https://github.com/fdehau)).
The original repository contains all the issues, PRs, and discussion that were raised originally, and The original repository contains all the issues, PRs and discussion that were raised originally, and
it is useful to refer to when contributing code, documentation, or issues with Ratatui. it is useful to refer to when contributing code, documentation, or issues with Ratatui.
We imported all the PRs from the original repository, implemented many of the smaller ones, and We imported all the PRs from the original repository and implemented many of the smaller ones and
made notes on the leftovers. These are marked as draft PRs and labelled as [imported from made notes on the leftovers. These are marked as draft PRs and labelled as [imported from
tui](https://github.com/ratatui/ratatui/pulls?q=is%3Apr+is%3Aopen+label%3A%22imported+from+tui%22). tui](https://github.com/ratatui-org/ratatui/pulls?q=is%3Apr+is%3Aopen+label%3A%22imported+from+tui%22).
We have documented the current state of those PRs, and anyone is welcome to pick them up and We have documented the current state of those PRs, and anyone is welcome to pick them up and
continue the work on them. continue the work on them.

View File

@@ -1,13 +1,11 @@
[package] [package]
name = "ratatui" name = "ratatui"
version = "0.28.1" # crate version version = "0.25.0" # crate version
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"] authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
description = "A library that's all about cooking up terminal user interfaces" description = "A library that's all about cooking up terminal user interfaces"
documentation = "https://docs.rs/ratatui/latest/ratatui/" documentation = "https://docs.rs/ratatui/latest/ratatui/"
repository = "https://github.com/ratatui/ratatui"
homepage = "https://ratatui.rs"
keywords = ["tui", "terminal", "dashboard"] keywords = ["tui", "terminal", "dashboard"]
categories = ["command-line-interface"] repository = "https://github.com/ratatui-org/ratatui"
readme = "README.md" readme = "README.md"
license = "MIT" license = "MIT"
exclude = [ exclude = [
@@ -18,99 +16,49 @@ exclude = [
"*.log", "*.log",
"tags", "tags",
] ]
autoexamples = true
edition = "2021" edition = "2021"
rust-version = "1.74.0" rust-version = "1.70.0"
[badges]
[dependencies] [dependencies]
crossterm = { version = "0.27", optional = true }
termion = { version = "3.0", optional = true }
termwiz = { version = "0.20.0", optional = true }
serde = { version = "1", optional = true, features = ["derive"] }
bitflags = "2.3" bitflags = "2.3"
cassowary = "0.3" cassowary = "0.3"
compact_str = "0.8.0" indoc = "2.0"
crossterm = { version = "0.28.1", optional = true } itertools = "0.12"
document-features = { version = "0.2.7", optional = true }
indoc = "2"
instability = "0.3.1"
itertools = "0.13"
lru = "0.12.0"
paste = "1.0.2" paste = "1.0.2"
palette = { version = "0.7.6", optional = true } strum = { version = "0.25", features = ["derive"] }
serde = { version = "1", optional = true, features = ["derive"] }
strum = { version = "0.26.3", features = ["derive"] }
termwiz = { version = "0.22.0", optional = true }
time = { version = "0.3.11", optional = true, features = ["local-offset"] } time = { version = "0.3.11", optional = true, features = ["local-offset"] }
unicode-segmentation = "1.10" unicode-segmentation = "1.10"
unicode-truncate = "1" unicode-width = "0.1"
unicode-width = "=0.1.13" document-features = { version = "0.2.7", optional = true }
lru = "0.12.0"
[target.'cfg(not(windows))'.dependencies] stability = "0.1.1"
# termion is not supported on Windows
termion = { version = "4.0.0", optional = true }
[dev-dependencies] [dev-dependencies]
anyhow = "1.0.71"
argh = "0.1.12" argh = "0.1.12"
better-panic = "0.3.0"
cargo-husky = { version = "1.5.0", default-features = false, features = [
"user-hooks",
] }
color-eyre = "0.6.2" color-eyre = "0.6.2"
criterion = { version = "0.5.1", features = ["html_reports"] } criterion = { version = "0.5.1", features = ["html_reports"] }
crossterm = { version = "0.28.1", features = ["event-stream"] } derive_builder = "0.12.0"
fakeit = "1.1" fakeit = "1.1"
font8x8 = "0.3.1" font8x8 = "0.3.1"
futures = "0.3.30" palette = "0.7.3"
indoc = "2"
octocrab = "0.41.0"
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"
rand = "0.8.5" rand = "0.8.5"
rand_chacha = "0.3.1" rand_chacha = "0.3.1"
rstest = "0.23.0" rstest = "0.18.2"
serde_json = "1.0.109" serde_json = "1.0.109"
tokio = { version = "1.39.2", features = [
"rt",
"macros",
"time",
"rt-multi-thread",
] }
tracing = "0.1.40"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
[lints.rust]
unsafe_code = "forbid"
[lints.clippy]
cargo = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
cast_possible_truncation = "allow"
cast_possible_wrap = "allow"
cast_precision_loss = "allow"
cast_sign_loss = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
module_name_repetitions = "allow"
must_use_candidate = "allow"
# we often split up a module into multiple files with the main type in a file named after the
# module, so we want to allow this pattern
module_inception = "allow"
# nursery or restricted
as_underscore = "warn"
deref_by_slicing = "warn"
else_if_without_else = "warn"
empty_line_after_doc_comments = "warn"
equatable_if_let = "warn"
fn_to_numeric_cast_any = "warn"
format_push_string = "warn"
map_err_ignore = "warn"
missing_const_for_fn = "warn"
mixed_read_write_in_expression = "warn"
mod_module_files = "warn"
needless_pass_by_ref_mut = "warn"
needless_raw_strings = "warn"
or_fun_call = "warn"
redundant_type_annotations = "warn"
rest_pat_in_fully_bound_structs = "warn"
string_lit_chars_any = "warn"
string_slice = "warn"
string_to_string = "warn"
unnecessary_self_imports = "warn"
use_self = "warn"
[features] [features]
#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file. #! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.
@@ -120,111 +68,97 @@ use_self = "warn"
## which allows you to set the underline color of text. ## which allows you to set the underline color of text.
default = ["crossterm", "underline-color"] default = ["crossterm", "underline-color"]
#! Generally an application will only use one backend, so you should only enable one of the following features: #! Generally an application will only use one backend, so you should only enable one of the following features:
## enables the [`CrosstermBackend`](backend::CrosstermBackend) backend and adds a dependency on [`crossterm`]. ## enables the [`CrosstermBackend`] backend and adds a dependency on the [Crossterm crate].
crossterm = ["dep:crossterm"] crossterm = ["dep:crossterm"]
## enables the [`TermionBackend`](backend::TermionBackend) backend and adds a dependency on [`termion`]. ## enables the [`TermionBackend`] backend and adds a dependency on the [Termion crate].
termion = ["dep:termion"] termion = ["dep:termion"]
## enables the [`TermwizBackend`](backend::TermwizBackend) backend and adds a dependency on [`termwiz`]. ## enables the [`TermwizBackend`] backend and adds a dependency on the [Termwiz crate].
termwiz = ["dep:termwiz"] termwiz = ["dep:termwiz"]
#! The following optional features are available for all backends: #! The following optional features are available for all backends:
## enables serialization and deserialization of style and color types using the [`serde`] crate. ## enables serialization and deserialization of style and color types using the [Serde crate].
## This is useful if you want to save themes to a file. ## This is useful if you want to save themes to a file.
serde = ["dep:serde", "bitflags/serde", "compact_str/serde"] serde = ["dep:serde", "bitflags/serde"]
## enables the [`border!`] macro. ## enables the [`border!`] macro.
macros = [] macros = []
## enables conversions from colors in the [`palette`] crate to [`Color`](crate::style::Color).
palette = ["dep:palette"]
## Use terminal scrolling regions to make some operations less prone to
## flickering. (i.e. Terminal::insert_before).
scrolling-regions = []
## enables all widgets. ## enables all widgets.
all-widgets = ["widget-calendar"] all-widgets = ["widget-calendar"]
#! Widgets that add dependencies are gated behind feature flags to prevent unused transitive #! Widgets that add dependencies are gated behind feature flags to prevent unused transitive
#! dependencies. The available features are: #! dependencies. The available features are:
## enables the [`calendar`](widgets::calendar) widget module and adds a dependency on [`time`]. ## enables the [`calendar`] widget module and adds a dependency on the [Time crate].
widget-calendar = ["dep:time"] widget-calendar = ["dep:time"]
#! The following optional features are only available for some backends: #! Underline color is only supported by the [`CrosstermBackend`] backend, and is not supported
#! on Windows 7.
## enables the backend code that sets the underline color. ## enables the backend code that sets the underline color.
## Underline color is only supported by the [`CrosstermBackend`](backend::CrosstermBackend) backend,
## and is not supported on Windows 7.
underline-color = ["dep:crossterm"] underline-color = ["dep:crossterm"]
#! The following features are unstable and may change in the future: #! The following features are unstable and may change in the future:
## Enable all unstable features. ## Enable all unstable features.
unstable = [ unstable = ["unstable-segment-size", "unstable-rendered-line-info"]
"unstable-rendered-line-info",
"unstable-widget-ref",
"unstable-backend-writer",
]
## Enables the [`Paragraph::line_count`](widgets::Paragraph::line_count) ## Enables the [`Layout::segment_size`](crate::layout::Layout::segment_size) method which is experimental and may change in the
## [`Paragraph::line_width`](widgets::Paragraph::line_width) methods ## future. See [Issue #536](https://github.com/ratatui-org/ratatui/issues/536) for more details.
unstable-segment-size = []
## Enables the [`Paragraph::line_count`](crate::widgets::Paragraph::line_count)
## [`Paragraph::line_width`](crate::widgets::Paragraph::line_width) methods
## which are experimental and may change in the future. ## which are experimental and may change in the future.
## See [Issue 293](https://github.com/ratatui/ratatui/issues/293) for more details. ## See [Issue 293](https://github.com/ratatui-org/ratatui/issues/293) for more details.
unstable-rendered-line-info = [] unstable-rendered-line-info = []
## Enables the [`WidgetRef`](widgets::WidgetRef) and [`StatefulWidgetRef`](widgets::StatefulWidgetRef) traits which are experimental and may change in
## the future.
unstable-widget-ref = []
## Enables getting access to backends' writers.
unstable-backend-writer = []
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
# see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html # see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
rustdoc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs"]
# Improve benchmark consistency [[bench]]
[profile.bench] name = "barchart"
codegen-units = 1 harness = false
lto = true
[[bench]]
name = "block"
harness = false
[[bench]]
name = "list"
harness = false
[lib] [lib]
bench = false bench = false
[[bench]] [[bench]]
name = "main" name = "paragraph"
harness = false
[[bench]]
name = "sparkline"
harness = false harness = false
[[example]]
name = "async"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]] [[example]]
name = "barchart" name = "barchart"
required-features = ["crossterm"] required-features = ["crossterm"]
doc-scrape-examples = true doc-scrape-examples = true
[[example]]
name = "barchart-grouped"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]] [[example]]
name = "block" name = "block"
required-features = ["crossterm"] required-features = ["crossterm"]
doc-scrape-examples = true doc-scrape-examples = true
[[example]] [[example]]
name = "calendar" name = "canvas"
required-features = ["crossterm", "widget-calendar"] required-features = ["crossterm"]
doc-scrape-examples = true doc-scrape-examples = true
[[example]] [[example]]
name = "canvas" name = "calendar"
required-features = ["crossterm"] required-features = ["crossterm", "widget-calendar"]
doc-scrape-examples = true doc-scrape-examples = true
[[example]] [[example]]
@@ -240,19 +174,9 @@ doc-scrape-examples = false
[[example]] [[example]]
name = "colors_rgb" name = "colors_rgb"
required-features = ["crossterm", "palette"]
doc-scrape-examples = true
[[example]]
name = "constraint-explorer"
required-features = ["crossterm"] required-features = ["crossterm"]
doc-scrape-examples = true doc-scrape-examples = true
[[example]]
name = "constraints"
required-features = ["crossterm"]
doc-scrape-examples = false
[[example]] [[example]]
name = "custom_widget" name = "custom_widget"
required-features = ["crossterm"] required-features = ["crossterm"]
@@ -265,7 +189,7 @@ doc-scrape-examples = false
[[example]] [[example]]
name = "demo2" name = "demo2"
required-features = ["crossterm", "palette", "widget-calendar"] required-features = ["crossterm", "widget-calendar"]
doc-scrape-examples = true doc-scrape-examples = true
[[example]] [[example]]
@@ -273,11 +197,6 @@ name = "docsrs"
required-features = ["crossterm"] required-features = ["crossterm"]
doc-scrape-examples = false doc-scrape-examples = false
[[example]]
name = "flex"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]] [[example]]
name = "gauge" name = "gauge"
required-features = ["crossterm"] required-features = ["crossterm"]
@@ -288,23 +207,18 @@ name = "hello_world"
required-features = ["crossterm"] required-features = ["crossterm"]
doc-scrape-examples = true doc-scrape-examples = true
[[example]]
name = "inline"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]] [[example]]
name = "layout" name = "layout"
required-features = ["crossterm"] required-features = ["crossterm"]
doc-scrape-examples = true doc-scrape-examples = true
[[example]] [[example]]
name = "line_gauge" name = "constraints"
required-features = ["crossterm"] required-features = ["crossterm"]
doc-scrape-examples = true doc-scrape-examples = false
[[example]] [[example]]
name = "hyperlink" name = "flex"
required-features = ["crossterm"] required-features = ["crossterm"]
doc-scrape-examples = true doc-scrape-examples = true
@@ -313,12 +227,6 @@ name = "list"
required-features = ["crossterm"] required-features = ["crossterm"]
doc-scrape-examples = true doc-scrape-examples = true
[[example]]
name = "minimal"
required-features = ["crossterm"]
# prefer to show the more featureful examples in the docs
doc-scrape-examples = false
[[example]] [[example]]
name = "modifiers" name = "modifiers"
required-features = ["crossterm"] required-features = ["crossterm"]
@@ -365,19 +273,14 @@ name = "tabs"
required-features = ["crossterm"] required-features = ["crossterm"]
doc-scrape-examples = true doc-scrape-examples = true
[[example]]
name = "tracing"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]] [[example]]
name = "user_input" name = "user_input"
required-features = ["crossterm"] required-features = ["crossterm"]
doc-scrape-examples = true doc-scrape-examples = true
[[example]] [[example]]
name = "widget_impl" name = "inline"
required-features = ["crossterm", "unstable-widget-ref"] required-features = ["crossterm"]
doc-scrape-examples = true doc-scrape-examples = true
[[test]] [[test]]

View File

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

View File

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

View File

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

View File

@@ -5,17 +5,23 @@ skip_core_tasks = true
[env] [env]
# all features except the backend ones # all features except the backend ones
NON_BACKEND_FEATURES = "all-widgets,macros,serde" ALL_FEATURES = "all-widgets,macros,serde"
# Windows does not support building termion, so this avoids the build failure by providing two
# sets of flags, one for Windows and one for other platforms.
# Windows: --features=all-widgets,macros,serde,crossterm,termwiz,underline-color
# Other: --features=all-widgets,macros,serde,crossterm,termion,termwiz,underline-color
ALL_FEATURES_FLAG = { source = "${CARGO_MAKE_RUST_TARGET_OS}", default_value = "--features=all-widgets,macros,serde,crossterm,termion,termwiz,unstable", mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz,unstable" } }
[tasks.default] [tasks.default]
alias = "ci" alias = "ci"
[tasks.ci] [tasks.ci]
description = "Run continuous integration tasks" description = "Run continuous integration tasks"
dependencies = ["lint", "clippy", "check", "test"] dependencies = ["lint-style", "clippy", "check", "test"]
[tasks.lint] [tasks.lint-style]
description = "Lint code style (formatting, typos, docs, markdown)" description = "Lint code style (formatting, typos, docs)"
dependencies = ["lint-format", "lint-typos", "lint-docs"] dependencies = ["lint-format", "lint-typos", "lint-docs"]
[tasks.lint-format] [tasks.lint-format]
@@ -41,27 +47,33 @@ toolchain = "nightly"
command = "cargo" command = "cargo"
args = [ args = [
"rustdoc", "rustdoc",
"--all-features", "--no-default-features",
"${ALL_FEATURES_FLAG}",
"--", "--",
"-Zunstable-options", "-Zunstable-options",
"--check", "--check",
"-Dwarnings", "-Dwarnings",
] ]
[tasks.lint-markdown]
description = "Check markdown files for errors and warnings"
command = "markdownlint-cli2"
args = ["**/*.md", "!target"]
[tasks.check] [tasks.check]
description = "Check code for errors and warnings" description = "Check code for errors and warnings"
command = "cargo" command = "cargo"
args = ["check", "--all-targets", "--all-features"] args = [
"check",
"--all-targets",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
]
[tasks.build] [tasks.build]
description = "Compile the project" description = "Compile the project"
command = "cargo" command = "cargo"
args = ["build", "--all-targets", "--all-features"] args = [
"build",
"--all-targets",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
]
[tasks.clippy] [tasks.clippy]
description = "Run Clippy for linting" description = "Run Clippy for linting"
@@ -69,9 +81,10 @@ command = "cargo"
args = [ args = [
"clippy", "clippy",
"--all-targets", "--all-targets",
"--all-features",
"--tests", "--tests",
"--benches", "--benches",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
"--", "--",
"-D", "-D",
"warnings", "warnings",
@@ -89,12 +102,18 @@ run_task = { name = ["test-lib", "test-doc"] }
description = "Run default tests" description = "Run default tests"
dependencies = ["install-nextest"] dependencies = ["install-nextest"]
command = "cargo" command = "cargo"
args = ["nextest", "run", "--all-targets", "--all-features"] args = [
"nextest",
"run",
"--all-targets",
"--no-default-features",
"${ALL_FEATURES_FLAG}",
]
[tasks.test-doc] [tasks.test-doc]
description = "Run documentation tests" description = "Run documentation tests"
command = "cargo" command = "cargo"
args = ["test", "--doc", "--all-features"] args = ["test", "--doc", "--no-default-features", "${ALL_FEATURES_FLAG}"]
[tasks.test-backend] [tasks.test-backend]
# takes a command line parameter to specify the backend to test (e.g. "crossterm") # takes a command line parameter to specify the backend to test (e.g. "crossterm")
@@ -107,7 +126,7 @@ args = [
"--all-targets", "--all-targets",
"--no-default-features", "--no-default-features",
"--features", "--features",
"${NON_BACKEND_FEATURES},${@}", "${ALL_FEATURES},${@}",
] ]
[tasks.coverage] [tasks.coverage]
@@ -118,7 +137,8 @@ args = [
"--lcov", "--lcov",
"--output-path", "--output-path",
"target/lcov.info", "target/lcov.info",
"--all-features", "--no-default-features",
"${ALL_FEATURES_FLAG}",
] ]
[tasks.run-example] [tasks.run-example]

238
README.md
View File

@@ -4,18 +4,14 @@
- [Ratatui](#ratatui) - [Ratatui](#ratatui)
- [Installation](#installation) - [Installation](#installation)
- [Introduction](#introduction) - [Introduction](#introduction)
- [Other documentation](#other-documentation) - [Other Documentation](#other-documentation)
- [Quickstart](#quickstart) - [Quickstart](#quickstart)
- [Initialize and restore the terminal](#initialize-and-restore-the-terminal)
- [Drawing the UI](#drawing-the-ui)
- [Handling events](#handling-events)
- [Example](#example)
- [Layout](#layout)
- [Text and styling](#text-and-styling)
- [Status of this fork](#status-of-this-fork) - [Status of this fork](#status-of-this-fork)
- [Rust version requirements](#rust-version-requirements)
- [Widgets](#widgets) - [Widgets](#widgets)
- [Built in](#built-in) - [Built in](#built-in)
- [Third-party libraries, bootstrapping templates and widgets](#third-party-libraries-bootstrapping-templates-and-widgets) - [Third\-party libraries, bootstrapping templates and
widgets](#third-party-libraries-bootstrapping-templates-and-widgets)
- [Apps](#apps) - [Apps](#apps)
- [Alternatives](#alternatives) - [Alternatives](#alternatives)
- [Acknowledgments](#acknowledgments) - [Acknowledgments](#acknowledgments)
@@ -25,14 +21,14 @@
<!-- cargo-rdme start --> <!-- cargo-rdme start -->
![Demo](https://github.com/ratatui/ratatui/blob/87ae72dbc756067c97f6400d3e2a58eeb383776e/examples/demo2-destroy.gif?raw=true) ![Demo](https://github.com/ratatui-org/ratatui/blob/1d39444e3dea6f309cf9035be2417ac711c1abc9/examples/demo2-destroy.gif?raw=true)
<div align="center"> <div align="center">
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![Deps.rs [![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
Badge]][Deps.rs]<br> [![Codecov Badge]][Codecov] [![License Badge]](./LICENSE) [![Sponsors Badge]](./LICENSE)<br>
Badge]][GitHub Sponsors]<br> [![Discord Badge]][Discord Server] [![Matrix Badge]][Matrix] [![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
[![Forum Badge]][Forum]<br> [![Matrix Badge]][Matrix]<br>
[Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br> [Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br>
[Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request] [Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request]
@@ -47,10 +43,10 @@ Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its deve
## Installation ## Installation
Add `ratatui` as a dependency to your cargo.toml: Add `ratatui` and `crossterm` as dependencies to your cargo.toml:
```shell ```shell
cargo add ratatui cargo add ratatui crossterm
``` ```
Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation] Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
@@ -65,13 +61,9 @@ This is in contrast to the retained mode style of rendering where widgets are up
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website] automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
for more info. for more info.
You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to
terminal user interfaces and showcases the features of Ratatui, along with a hello world demo.
## Other documentation ## Other documentation
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials - [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
- [Ratatui Forum][Forum] - a place to ask questions and discuss the library
- [API Docs] - the full API documentation for the library on docs.rs. - [API Docs] - the full API documentation for the library on docs.rs.
- [Examples] - a collection of examples that demonstrate how to use the library. - [Examples] - a collection of examples that demonstrate how to use the library.
- [Contributing] - Please read this if you are interested in contributing to the project. - [Contributing] - Please read this if you are interested in contributing to the project.
@@ -115,8 +107,7 @@ module] and the [Backends] section of the [Ratatui Website] for more info.
The drawing logic is delegated to a closure that takes a [`Frame`] instance as argument. The The drawing logic is delegated to a closure that takes a [`Frame`] instance as argument. The
[`Frame`] provides the size of the area to draw to and allows the app to render any [`Widget`] [`Frame`] provides the size of the area to draw to and allows the app to render any [`Widget`]
using the provided [`render_widget`] method. After this closure returns, a diff is performed and using the provided [`render_widget`] method. See the [Widgets] section of the [Ratatui Website]
only the changes are drawn to the terminal. See the [Widgets] section of the [Ratatui Website]
for more info. for more info.
### Handling events ### Handling events
@@ -131,18 +122,12 @@ Website] for more info. For example, if you are using [Crossterm], you can use t
```rust ```rust
use std::io::{self, stdout}; use std::io::{self, stdout};
use ratatui::{ use crossterm::{
backend::CrosstermBackend, event::{self, Event, KeyCode},
crossterm::{ terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
event::{self, Event, KeyCode}, ExecutableCommand,
terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
},
ExecutableCommand,
},
widgets::{Block, Paragraph},
Frame, Terminal,
}; };
use ratatui::{prelude::*, widgets::*};
fn main() -> io::Result<()> { fn main() -> io::Result<()> {
enable_raw_mode()?; enable_raw_mode()?;
@@ -173,8 +158,9 @@ fn handle_events() -> io::Result<bool> {
fn ui(frame: &mut Frame) { fn ui(frame: &mut Frame) {
frame.render_widget( frame.render_widget(
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")), Paragraph::new("Hello World!")
frame.area(), .block(Block::default().title("Greeting").borders(Borders::ALL)),
frame.size(),
); );
} }
``` ```
@@ -191,27 +177,40 @@ area. This lets you describe a responsive terminal UI by nesting layouts. See th
section of the [Ratatui Website] for more info. section of the [Ratatui Website] for more info.
```rust ```rust
use ratatui::{ use ratatui::{prelude::*, widgets::*};
layout::{Constraint, Layout},
widgets::Block,
Frame,
};
fn ui(frame: &mut Frame) { fn ui(frame: &mut Frame) {
let [title_area, main_area, status_area] = Layout::vertical([ let main_layout = Layout::new(
Constraint::Length(1), Direction::Vertical,
Constraint::Min(0), [
Constraint::Length(1), Constraint::Length(1),
]) Constraint::Min(0),
.areas(frame.area()); Constraint::Length(1),
let [left_area, right_area] = ],
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]) )
.areas(main_area); .split(frame.size());
frame.render_widget(
Block::new().borders(Borders::TOP).title("Title Bar"),
main_layout[0],
);
frame.render_widget(
Block::new().borders(Borders::TOP).title("Status Bar"),
main_layout[2],
);
frame.render_widget(Block::bordered().title("Title Bar"), title_area); let inner_layout = Layout::new(
frame.render_widget(Block::bordered().title("Status Bar"), status_area); Direction::Horizontal,
frame.render_widget(Block::bordered().title("Left"), left_area); [Constraint::Percentage(50), Constraint::Percentage(50)],
frame.render_widget(Block::bordered().title("Right"), right_area); )
.split(main_layout[1]);
frame.render_widget(
Block::default().borders(Borders::ALL).title("Left"),
inner_layout[0],
);
frame.render_widget(
Block::default().borders(Borders::ALL).title("Right"),
inner_layout[1],
);
} }
``` ```
@@ -232,41 +231,48 @@ short-hand syntax to apply a style to widgets and text. See the [Styling Text] s
[Ratatui Website] for more info. [Ratatui Website] for more info.
```rust ```rust
use ratatui::{ use ratatui::{prelude::*, widgets::*};
layout::{Constraint, Layout},
style::{Color, Modifier, Style, Stylize},
text::{Line, Span},
widgets::{Block, Paragraph},
Frame,
};
fn ui(frame: &mut Frame) { fn ui(frame: &mut Frame) {
let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.area()); let areas = Layout::new(
Direction::Vertical,
[
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
],
)
.split(frame.size());
let line = Line::from(vec![ let span1 = Span::raw("Hello ");
Span::raw("Hello "), let span2 = Span::styled(
Span::styled( "World",
"World", Style::new()
Style::new() .fg(Color::Green)
.fg(Color::Green) .bg(Color::White)
.bg(Color::White) .add_modifier(Modifier::BOLD),
.add_modifier(Modifier::BOLD), );
), let span3 = "!".red().on_light_yellow().italic();
"!".red().on_light_yellow().italic(),
]);
frame.render_widget(line, areas[0]);
// using the short-hand syntax and implicit conversions let line = Line::from(vec![span1, span2, span3]);
let paragraph = Paragraph::new("Hello World!".red().on_white().bold()); let text: Text = Text::from(vec![line]);
frame.render_widget(paragraph, areas[1]);
// style the whole widget instead of just the text frame.render_widget(Paragraph::new(text), areas[0]);
let paragraph = Paragraph::new("Hello World!").style(Style::new().red().on_white()); // or using the short-hand syntax and implicit conversions
frame.render_widget(paragraph, areas[2]); frame.render_widget(
Paragraph::new("Hello World!".red().on_white().bold()),
areas[1],
);
// use the simpler short-hand syntax // to style the whole widget instead of just the text
let paragraph = Paragraph::new("Hello World!").blue().on_yellow(); frame.render_widget(
frame.render_widget(paragraph, areas[3]); Paragraph::new("Hello World!").style(Style::new().red().on_white()),
areas[2],
);
// or using the short-hand syntax
frame.render_widget(Paragraph::new("Hello World!").blue().on_yellow(), areas[3]);
} }
``` ```
@@ -284,21 +290,20 @@ Running this example produces the following output:
[Handling Events]: https://ratatui.rs/concepts/event-handling/ [Handling Events]: https://ratatui.rs/concepts/event-handling/
[Layout]: https://ratatui.rs/how-to/layout/ [Layout]: https://ratatui.rs/how-to/layout/
[Styling Text]: https://ratatui.rs/how-to/render/style-text/ [Styling Text]: https://ratatui.rs/how-to/render/style-text/
[templates]: https://github.com/ratatui/templates/ [templates]: https://github.com/ratatui-org/templates/
[Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md [Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples/README.md
[Report a bug]: https://github.com/ratatui/ratatui/issues/new?labels=bug&projects=&template=bug_report.md [Report a bug]: https://github.com/ratatui-org/ratatui/issues/new?labels=bug&projects=&template=bug_report.md
[Request a Feature]: https://github.com/ratatui/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md [Request a Feature]: https://github.com/ratatui-org/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md
[Create a Pull Request]: https://github.com/ratatui/ratatui/compare [Create a Pull Request]: https://github.com/ratatui-org/ratatui/compare
[git-cliff]: https://git-cliff.org [git-cliff]: https://git-cliff.org
[Conventional Commits]: https://www.conventionalcommits.org [Conventional Commits]: https://www.conventionalcommits.org
[API Docs]: https://docs.rs/ratatui [API Docs]: https://docs.rs/ratatui
[Changelog]: https://github.com/ratatui/ratatui/blob/main/CHANGELOG.md [Changelog]: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md
[Contributing]: https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md [Contributing]: https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md
[Breaking Changes]: https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md [Breaking Changes]: https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md
[FOSDEM 2024 talk]: https://www.youtube.com/watch?v=NU0q6NOLJ20 [docsrs-hello]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
[docsrs-hello]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true [docsrs-layout]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
[docsrs-layout]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true [docsrs-styling]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
[docsrs-styling]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
[`Frame`]: terminal::Frame [`Frame`]: terminal::Frame
[`render_widget`]: terminal::Frame::render_widget [`render_widget`]: terminal::Frame::render_widget
[`Widget`]: widgets::Widget [`Widget`]: widgets::Widget
@@ -317,23 +322,23 @@ Running this example produces the following output:
[Termion]: https://crates.io/crates/termion [Termion]: https://crates.io/crates/termion
[Termwiz]: https://crates.io/crates/termwiz [Termwiz]: https://crates.io/crates/termwiz
[tui-rs]: https://crates.io/crates/tui [tui-rs]: https://crates.io/crates/tui
[GitHub Sponsors]: https://github.com/sponsors/ratatui [Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square&logoColor=E05D44&color=E05D44 [License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square&color=1370D3 [CI Badge]:
[CI Badge]: https://img.shields.io/github/actions/workflow/status/ratatui/ratatui/ci.yml?style=flat-square&logo=github https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui/ci.yml?style=flat-square&logo=github
[CI Workflow]: https://github.com/ratatui/ratatui/actions/workflows/ci.yml [CI Workflow]: https://github.com/ratatui-org/ratatui/actions/workflows/ci.yml
[Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3 [Codecov Badge]:
[Codecov]: https://app.codecov.io/gh/ratatui/ratatui https://img.shields.io/codecov/c/github/ratatui-org/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST
[Deps.rs Badge]: https://deps.rs/repo/github/ratatui/ratatui/status.svg?style=flat-square [Codecov]: https://app.codecov.io/gh/ratatui-org/ratatui
[Deps.rs]: https://deps.rs/repo/github/ratatui/ratatui [Deps.rs Badge]: https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square
[Discord Badge]: https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3 [Deps.rs]: https://deps.rs/repo/github/ratatui-org/ratatui
[Discord Badge]:
https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square
[Discord Server]: https://discord.gg/pMCEU9hNEj [Discord Server]: https://discord.gg/pMCEU9hNEj
[Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square&logoColor=E05D44 [Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square
[Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3 [Matrix Badge]:
https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix
[Matrix]: https://matrix.to/#/#ratatui:matrix.org [Matrix]: https://matrix.to/#/#ratatui:matrix.org
[Forum Badge]: https://img.shields.io/discourse/likes?server=https%3A%2F%2Fforum.ratatui.rs&style=flat-square&logo=discourse&label=forum&color=C43AC3
[Forum]: https://forum.ratatui.rs
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui?logo=github&style=flat-square&color=1370D3
<!-- cargo-rdme end --> <!-- cargo-rdme end -->
@@ -348,14 +353,17 @@ In order to organize ourselves, we currently use a [Discord server](https://disc
feel free to join and come chat! There is also a [Matrix](https://matrix.org/) bridge available at feel free to join and come chat! There is also a [Matrix](https://matrix.org/) bridge available at
[#ratatui:matrix.org](https://matrix.to/#/#ratatui:matrix.org). [#ratatui:matrix.org](https://matrix.to/#/#ratatui:matrix.org).
While we do utilize Discord for coordinating, it's not essential for contributing. We have recently While we do utilize Discord for coordinating, it's not essential for contributing.
launched the [Ratatui Forum][Forum], and our primary open-source workflow is centered around GitHub. Our primary open-source workflow is centered around GitHub.
For bugs and features, we rely on GitHub. Please [Report a bug], [Request a Feature] or [Create a For significant discussions, we rely on GitHub — please open an issue, a discussion or a PR.
Pull Request].
Please make sure you read the updated [contributing](./CONTRIBUTING.md) guidelines, especially if Please make sure you read the updated [contributing](./CONTRIBUTING.md) guidelines, especially if
you are interested in working on a PR or issue opened in the previous repository. you are interested in working on a PR or issue opened in the previous repository.
## Rust version requirements
Since version 0.23.0, The Minimum Supported Rust Version (MSRV) of `ratatui` is 1.67.0.
## Widgets ## Widgets
### Built in ### Built in
@@ -391,7 +399,7 @@ be installed with `cargo install cargo-make`).
`ratatui::text::Text` `ratatui::text::Text`
- [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to - [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
`ratatui::style::Color` `ratatui::style::Color`
- [templates](https://github.com/ratatui/templates) — Starter templates for - [templates](https://github.com/ratatui-org/templates) — Starter templates for
bootstrapping a Rust TUI application with Ratatui & crossterm bootstrapping a Rust TUI application with Ratatui & crossterm
- [tui-builder](https://github.com/jkelleyrtp/tui-builder) — Batteries-included MVC framework for - [tui-builder](https://github.com/jkelleyrtp/tui-builder) — Batteries-included MVC framework for
Tui-rs + Crossterm apps Tui-rs + Crossterm apps
@@ -415,7 +423,7 @@ be installed with `cargo install cargo-make`).
## Apps ## Apps
Check out [awesome-ratatui](https://github.com/ratatui/awesome-ratatui) for a curated list of Check out [awesome-ratatui](https://github.com/ratatui-org/awesome-ratatui) for a curated list of
awesome apps/libraries built with `ratatui`! awesome apps/libraries built with `ratatui`!
## Alternatives ## Alternatives
@@ -426,7 +434,7 @@ to build text user interfaces in Rust.
## Acknowledgments ## Acknowledgments
Special thanks to [**Pavel Fomchenkov**](https://github.com/nawok) for his work in designing **an Special thanks to [**Pavel Fomchenkov**](https://github.com/nawok) for his work in designing **an
awesome logo** for the ratatui project and ratatui organization. awesome logo** for the ratatui project and ratatui-org organization.
## License ## License

View File

@@ -1,12 +1,5 @@
# Creating a Release # Creating a Release
Our release strategy is:
> Release major versions with detailed summaries when necessary, while releasing minor versions
> weekly or as needed without extensive announcements.
>
> Versioning scheme being `0.x.y`, where `x` is the major version and `y` is the minor version.
[crates.io](https://crates.io/crates/ratatui) releases are automated via [GitHub [crates.io](https://crates.io/crates/ratatui) releases are automated via [GitHub
actions](.github/workflows/cd.yml) and triggered by pushing a tag. actions](.github/workflows/cd.yml) and triggered by pushing a tag.
@@ -19,7 +12,7 @@ actions](.github/workflows/cd.yml) and triggered by pushing a tag.
``` ```
1. Switch branches to the images branch and copy demo2.gif to examples/, commit, and push. 1. Switch branches to the images branch and copy demo2.gif to examples/, commit, and push.
1. Grab the permalink from <https://github.com/ratatui/ratatui/blob/images/examples/demo2.gif> and 1. Grab the permalink from <https://github.com/ratatui-org/ratatui/blob/images/examples/demo2.gif> and
append `?raw=true` to redirect to the actual image url. Then update the link in the main README. append `?raw=true` to redirect to the actual image url. Then update the link in the main README.
Avoid adding the gif to the git repo as binary files tend to bloat repositories. Avoid adding the gif to the git repo as binary files tend to bloat repositories.
@@ -28,16 +21,16 @@ actions](.github/workflows/cd.yml) and triggered by pushing a tag.
can be used for generating the entries. can be used for generating the entries.
1. Ensure that any breaking changes are documented in [BREAKING-CHANGES.md](./BREAKING-CHANGES.md) 1. Ensure that any breaking changes are documented in [BREAKING-CHANGES.md](./BREAKING-CHANGES.md)
1. Commit and push the changes. 1. Commit and push the changes.
1. Create a new tag: `git tag -a v[0.x.y]` 1. Create a new tag: `git tag -a v[X.Y.Z]`
1. Push the tag: `git push --tags` 1. Push the tag: `git push --tags`
1. Wait for [Continuous Deployment](https://github.com/ratatui/ratatui/actions) workflow to 1. Wait for [Continuous Deployment](https://github.com/ratatui-org/ratatui/actions) workflow to
finish. finish.
## Alpha Releases ## Alpha Releases
Alpha releases are automatically released every Saturday via [cd.yml](./.github/workflows/cd.yml) Alpha releases are automatically released every Saturday via [cd.yml](./.github/workflows/cd.yml)
and can be manually be created when necessary by triggering the [Continuous and can be manually be created when necessary by triggering the [Continuous
Deployment](https://github.com/ratatui/ratatui/actions/workflows/cd.yml) workflow. Deployment](https://github.com/ratatui-org/ratatui/actions/workflows/cd.yml) workflow.
We automatically release an alpha release with a patch level bump + alpha.num weekly (and when we We automatically release an alpha release with a patch level bump + alpha.num weekly (and when we
need to manually). E.g. the last release was 0.22.0, and the most recent alpha release is need to manually). E.g. the last release was 0.22.0, and the most recent alpha release is
@@ -47,5 +40,5 @@ These releases will have whatever happened to be in main at the time of release,
for apps that need to get releases from crates.io, but may contain more bugs and be generally less for apps that need to get releases from crates.io, but may contain more bugs and be generally less
tested than normal releases. tested than normal releases.
See [#147](https://github.com/ratatui/ratatui/issues/147) and See [#147](https://github.com/ratatui-org/ratatui/issues/147) and
[#359](https://github.com/ratatui/ratatui/pull/359) for more info on the alpha release process. [#359](https://github.com/ratatui-org/ratatui/pull/359) for more info on the alpha release process.

View File

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

View File

@@ -44,16 +44,6 @@ command = [
] ]
need_stdout = true need_stdout = true
[jobs.test-unit]
command = [
"cargo", "test",
"--lib",
"--all-features",
"--color", "always",
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
]
need_stdout = true
[jobs.doc] [jobs.doc]
command = [ command = [
"cargo", "+nightly", "doc", "cargo", "+nightly", "doc",
@@ -84,7 +74,7 @@ on_success = "job:doc" # so that we don't open the browser at each change
command = [ command = [
"cargo", "llvm-cov", "cargo", "llvm-cov",
"--lcov", "--output-path", "target/lcov.info", "--lcov", "--output-path", "target/lcov.info",
"--all-features", "--all-features",
"--color", "always", "--color", "always",
] ]
@@ -107,6 +97,4 @@ ctrl-c = "job:check-crossterm"
ctrl-t = "job:check-termion" ctrl-t = "job:check-termion"
ctrl-w = "job:check-termwiz" ctrl-w = "job:check-termwiz"
v = "job:coverage" v = "job:coverage"
ctrl-v = "job:coverage-unit-tests-only" u = "job:coverage-unit-tests-only"
u = "job:test-unit"
n = "job:nextest"

View File

@@ -1,13 +1,14 @@
use criterion::{criterion_group, Bencher, BenchmarkId, Criterion}; use criterion::{criterion_group, criterion_main, Bencher, BenchmarkId, Criterion};
use rand::Rng; use rand::Rng;
use ratatui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
layout::{Direction, Rect}, layout::Rect,
prelude::Direction,
widgets::{Bar, BarChart, BarGroup, Widget}, widgets::{Bar, BarChart, BarGroup, Widget},
}; };
/// Benchmark for rendering a barchart. /// Benchmark for rendering a barchart.
fn barchart(c: &mut Criterion) { pub fn barchart(c: &mut Criterion) {
let mut group = c.benchmark_group("barchart"); let mut group = c.benchmark_group("barchart");
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
@@ -58,14 +59,15 @@ fn barchart(c: &mut Criterion) {
fn render(bencher: &mut Bencher, barchart: &BarChart) { fn render(bencher: &mut Bencher, barchart: &BarChart) {
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50)); let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
// We use `iter_batched` to clone the value in the setup function. // We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui/ratatui/pull/377. // See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched( bencher.iter_batched(
|| barchart.clone(), || barchart.clone(),
|bench_barchart| { |bench_barchart| {
bench_barchart.render(buffer.area, &mut buffer); bench_barchart.render(buffer.area, &mut buffer);
}, },
criterion::BatchSize::LargeInput, criterion::BatchSize::LargeInput,
); )
} }
criterion_group!(benches, barchart); criterion_group!(benches, barchart);
criterion_main!(benches);

64
benches/block.rs Normal file
View File

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

View File

@@ -1,4 +1,4 @@
use criterion::{criterion_group, BatchSize, Bencher, BenchmarkId, Criterion}; use criterion::{criterion_group, criterion_main, BatchSize, Bencher, BenchmarkId, Criterion};
use ratatui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
layout::Rect, layout::Rect,
@@ -7,7 +7,7 @@ use ratatui::{
/// Benchmark for rendering a list. /// Benchmark for rendering a list.
/// It only benchmarks the render with a different amount of items. /// It only benchmarks the render with a different amount of items.
fn list(c: &mut Criterion) { pub fn list(c: &mut Criterion) {
let mut group = c.benchmark_group("list"); let mut group = c.benchmark_group("list");
for line_count in [64, 2048, 16384] { for line_count in [64, 2048, 16384] {
@@ -33,7 +33,7 @@ fn list(c: &mut Criterion) {
ListState::default() ListState::default()
.with_offset(line_count / 2) .with_offset(line_count / 2)
.with_selected(Some(line_count / 2)), .with_selected(Some(line_count / 2)),
); )
}, },
); );
} }
@@ -45,28 +45,29 @@ fn list(c: &mut Criterion) {
fn render(bencher: &mut Bencher, list: &List) { fn render(bencher: &mut Bencher, list: &List) {
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50)); let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
// We use `iter_batched` to clone the value in the setup function. // We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui/ratatui/pull/377. // See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched( bencher.iter_batched(
|| list.to_owned(), || list.to_owned(),
|bench_list| { |bench_list| {
Widget::render(bench_list, buffer.area, &mut buffer); Widget::render(bench_list, buffer.area, &mut buffer);
}, },
BatchSize::LargeInput, BatchSize::LargeInput,
); )
} }
/// render the list into a common size buffer with a state /// render the list into a common size buffer with a state
fn render_stateful(bencher: &mut Bencher, list: &List, mut state: ListState) { fn render_stateful(bencher: &mut Bencher, list: &List, mut state: ListState) {
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50)); let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
// We use `iter_batched` to clone the value in the setup function. // We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui/ratatui/pull/377. // See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched( bencher.iter_batched(
|| list.to_owned(), || list.to_owned(),
|bench_list| { |bench_list| {
StatefulWidget::render(bench_list, buffer.area, &mut buffer, &mut state); StatefulWidget::render(bench_list, buffer.area, &mut buffer, &mut state);
}, },
BatchSize::LargeInput, BatchSize::LargeInput,
); )
} }
criterion_group!(benches, list); criterion_group!(benches, list);
criterion_main!(benches);

View File

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

View File

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

View File

@@ -1,97 +0,0 @@
use std::iter::zip;
use criterion::{black_box, BenchmarkId, Criterion};
use ratatui::{
buffer::{Buffer, Cell},
layout::Rect,
text::Line,
widgets::Widget,
};
criterion::criterion_group!(benches, empty, filled, with_lines, diff);
const fn rect(size: u16) -> Rect {
Rect::new(0, 0, size, size)
}
fn empty(c: &mut Criterion) {
let mut group = c.benchmark_group("buffer/empty");
for size in [16, 64, 255] {
let area = rect(size);
group.bench_with_input(BenchmarkId::from_parameter(size), &area, |b, &area| {
b.iter(|| {
let _buffer = Buffer::empty(black_box(area));
});
});
}
group.finish();
}
/// This likely should have the same performance as `empty`, but it's here for completeness
/// and to catch any potential performance regressions.
fn filled(c: &mut Criterion) {
let mut group = c.benchmark_group("buffer/filled");
for size in [16, 64, 255] {
let area = rect(size);
let cell = Cell::new("AAAA"); // simulate a multi-byte character
group.bench_with_input(
BenchmarkId::from_parameter(size),
&(area, cell),
|b, (area, cell)| {
b.iter(|| {
let _buffer = Buffer::filled(black_box(*area), cell.clone());
});
},
);
}
group.finish();
}
fn with_lines(c: &mut Criterion) {
let mut group = c.benchmark_group("buffer/with_lines");
for size in [16, 64, 255] {
let word_count = 50;
let lines = fakeit::words::sentence(word_count);
let lines = lines.lines().map(Line::from);
group.bench_with_input(BenchmarkId::from_parameter(size), &lines, |b, lines| {
b.iter(|| {
let _buffer = Buffer::with_lines(black_box(lines.clone()));
});
});
}
group.finish();
}
fn diff(c: &mut Criterion) {
const AREA: Rect = Rect {
x: 0,
y: 0,
width: 200,
height: 50,
};
c.bench_function("buffer/diff", |b| {
let buffer_1 = create_random_buffer(AREA);
let buffer_2 = create_random_buffer(AREA);
b.iter(|| {
let _ = black_box(&buffer_1).diff(black_box(&buffer_2));
});
});
}
fn create_random_buffer(area: Rect) -> Buffer {
const PARAGRAPH_COUNT: i64 = 15;
const SENTENCE_COUNT: i64 = 5;
const WORD_COUNT: i64 = 20;
const SEPARATOR: &str = "\n\n";
let paragraphs = fakeit::words::paragraph(
PARAGRAPH_COUNT,
SENTENCE_COUNT,
WORD_COUNT,
SEPARATOR.to_string(),
);
let mut buffer = Buffer::empty(area);
for (line, row) in zip(paragraphs.lines(), area.rows()) {
Line::from(line).render(row, &mut buffer);
}
buffer
}

View File

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

View File

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

View File

@@ -1,69 +0,0 @@
use criterion::{criterion_group, BatchSize, Bencher, BenchmarkId, Criterion};
use ratatui::{
buffer::Buffer,
layout::{Constraint, Rect},
widgets::{Row, StatefulWidget, Table, TableState, Widget},
};
/// Benchmark for rendering a table.
/// It only benchmarks the render with a different number of rows, and columns.
fn table(c: &mut Criterion) {
let mut group = c.benchmark_group("table");
for row_count in [64, 2048, 16384] {
for col_count in [2, 4, 8] {
let bench_sizes = format!("{row_count}x{col_count}");
let rows: Vec<Row> = (0..row_count)
.map(|_| Row::new((0..col_count).map(|_| fakeit::words::quote())))
.collect();
// Render default table
group.bench_with_input(
BenchmarkId::new("render", &bench_sizes),
&Table::new(rows.clone(), [] as [Constraint; 0]),
render,
);
// Render with an offset to the middle of the table and a selected row
group.bench_with_input(
BenchmarkId::new("render_scroll_half", &bench_sizes),
&Table::new(rows, [] as [Constraint; 0]).highlight_symbol(">>"),
|b, table| {
render_stateful(
b,
table,
TableState::default()
.with_offset(row_count / 2)
.with_selected(Some(row_count / 2)),
);
},
);
}
}
group.finish();
}
fn render(bencher: &mut Bencher, table: &Table) {
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
bencher.iter_batched(
|| table.to_owned(),
|bench_table| {
Widget::render(bench_table, buffer.area, &mut buffer);
},
BatchSize::LargeInput,
);
}
fn render_stateful(bencher: &mut Bencher, table: &Table, mut state: TableState) {
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
bencher.iter_batched(
|| table.to_owned(),
|bench_table| {
StatefulWidget::render(bench_table, buffer.area, &mut buffer, &mut state);
},
BatchSize::LargeInput,
);
}
criterion_group!(benches, table);

View File

@@ -1,4 +1,6 @@
use criterion::{black_box, criterion_group, BatchSize, Bencher, BenchmarkId, Criterion}; use criterion::{
black_box, criterion_group, criterion_main, BatchSize, Bencher, BenchmarkId, Criterion,
};
use ratatui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
layout::Rect, layout::Rect,
@@ -15,15 +17,15 @@ const WRAP_WIDTH: u16 = 100;
/// Benchmark for rendering a paragraph with a given number of lines. The design of this benchmark /// Benchmark for rendering a paragraph with a given number of lines. The design of this benchmark
/// allows comparison of the performance of rendering a paragraph with different numbers of lines. /// allows comparison of the performance of rendering a paragraph with different numbers of lines.
/// as well as comparing with the various settings on the scroll and wrap features. /// as well as comparing with the various settings on the scroll and wrap features.
fn paragraph(c: &mut Criterion) { pub fn paragraph(c: &mut Criterion) {
let mut group = c.benchmark_group("paragraph"); let mut group = c.benchmark_group("paragraph");
for line_count in [64, 2048, MAX_SCROLL_OFFSET] { for &line_count in [64, 2048, MAX_SCROLL_OFFSET].iter() {
let lines = random_lines(line_count); let lines = random_lines(line_count);
let lines = lines.as_str(); let lines = lines.as_str();
// benchmark that measures the overhead of creating a paragraph separately from rendering // benchmark that measures the overhead of creating a paragraph separately from rendering
group.bench_with_input(BenchmarkId::new("new", line_count), lines, |b, lines| { group.bench_with_input(BenchmarkId::new("new", line_count), lines, |b, lines| {
b.iter(|| Paragraph::new(black_box(lines))); b.iter(|| Paragraph::new(black_box(lines)))
}); });
// render the paragraph with no scroll // render the paragraph with no scroll
@@ -36,14 +38,14 @@ fn paragraph(c: &mut Criterion) {
// scroll the paragraph by half the number of lines and render // scroll the paragraph by half the number of lines and render
group.bench_with_input( group.bench_with_input(
BenchmarkId::new("render_scroll_half", line_count), BenchmarkId::new("render_scroll_half", line_count),
&Paragraph::new(lines).scroll((0, line_count / 2)), &Paragraph::new(lines).scroll((0u16, line_count / 2)),
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH), |bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
); );
// scroll the paragraph by the full number of lines and render // scroll the paragraph by the full number of lines and render
group.bench_with_input( group.bench_with_input(
BenchmarkId::new("render_scroll_full", line_count), BenchmarkId::new("render_scroll_full", line_count),
&Paragraph::new(lines).scroll((0, line_count)), &Paragraph::new(lines).scroll((0u16, line_count)),
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH), |bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
); );
@@ -59,7 +61,7 @@ fn paragraph(c: &mut Criterion) {
BenchmarkId::new("render_wrap_scroll_full", line_count), BenchmarkId::new("render_wrap_scroll_full", line_count),
&Paragraph::new(lines) &Paragraph::new(lines)
.wrap(Wrap { trim: false }) .wrap(Wrap { trim: false })
.scroll((0, line_count)), .scroll((0u16, line_count)),
|bencher, paragraph| render(bencher, paragraph, WRAP_WIDTH), |bencher, paragraph| render(bencher, paragraph, WRAP_WIDTH),
); );
} }
@@ -70,14 +72,14 @@ fn paragraph(c: &mut Criterion) {
fn render(bencher: &mut Bencher, paragraph: &Paragraph, width: u16) { fn render(bencher: &mut Bencher, paragraph: &Paragraph, width: u16) {
let mut buffer = Buffer::empty(Rect::new(0, 0, width, 50)); let mut buffer = Buffer::empty(Rect::new(0, 0, width, 50));
// We use `iter_batched` to clone the value in the setup function. // We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui/ratatui/pull/377. // See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched( bencher.iter_batched(
|| paragraph.to_owned(), || paragraph.to_owned(),
|bench_paragraph| { |bench_paragraph| {
bench_paragraph.render(buffer.area, &mut buffer); bench_paragraph.render(buffer.area, &mut buffer);
}, },
BatchSize::LargeInput, BatchSize::LargeInput,
); )
} }
/// Create a string with the given number of lines filled with nonsense words /// Create a string with the given number of lines filled with nonsense words
@@ -85,10 +87,11 @@ fn render(bencher: &mut Bencher, paragraph: &Paragraph, width: u16) {
/// English language has about 5.1 average characters per word so including the space between words /// English language has about 5.1 average characters per word so including the space between words
/// this should emit around 200 characters per paragraph on average. /// this should emit around 200 characters per paragraph on average.
fn random_lines(count: u16) -> String { fn random_lines(count: u16) -> String {
let count = i64::from(count); let count = count as i64;
let sentence_count = 3; let sentence_count = 3;
let word_count = 11; let word_count = 11;
fakeit::words::paragraph(count, sentence_count, word_count, "\n".into()) fakeit::words::paragraph(count, sentence_count, word_count, "\n".into())
} }
criterion_group!(benches, paragraph); criterion_group!(benches, paragraph);
criterion_main!(benches);

View File

@@ -1,4 +1,4 @@
use criterion::{criterion_group, Bencher, BenchmarkId, Criterion}; use criterion::{criterion_group, criterion_main, Bencher, BenchmarkId, Criterion};
use rand::Rng; use rand::Rng;
use ratatui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
@@ -7,7 +7,7 @@ use ratatui::{
}; };
/// Benchmark for rendering a sparkline. /// Benchmark for rendering a sparkline.
fn sparkline(c: &mut Criterion) { pub fn sparkline(c: &mut Criterion) {
let mut group = c.benchmark_group("sparkline"); let mut group = c.benchmark_group("sparkline");
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
@@ -31,14 +31,15 @@ fn sparkline(c: &mut Criterion) {
fn render(bencher: &mut Bencher, sparkline: &Sparkline) { fn render(bencher: &mut Bencher, sparkline: &Sparkline) {
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50)); let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
// We use `iter_batched` to clone the value in the setup function. // We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui/ratatui/pull/377. // See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched( bencher.iter_batched(
|| sparkline.clone(), || sparkline.clone(),
|bench_sparkline| { |bench_sparkline| {
bench_sparkline.render(buffer.area, &mut buffer); bench_sparkline.render(buffer.area, &mut buffer);
}, },
criterion::BatchSize::LargeInput, criterion::BatchSize::LargeInput,
); )
} }
criterion_group!(benches, sparkline); criterion_group!(benches, sparkline);
criterion_main!(benches);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,136 +1,277 @@
//! # [Ratatui] `BarChart` example use std::{
//! error::Error,
//! The latest version of this example is available in the [examples] folder in the repository. io,
//! time::{Duration, Instant},
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use color_eyre::Result;
use rand::{thread_rng, Rng};
use ratatui::{
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Direction, Layout},
style::{Color, Style, Stylize},
text::Line,
widgets::{Bar, BarChart, BarGroup, Block},
DefaultTerminal, Frame,
}; };
fn main() -> Result<()> { use crossterm::{
color_eyre::install()?; event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
let terminal = ratatui::init(); execute,
let app_result = App::new().run(terminal); terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ratatui::restore(); };
app_result use ratatui::{prelude::*, widgets::*};
struct Company<'a> {
revenue: [u64; 4],
label: &'a str,
bar_style: Style,
} }
struct App { struct App<'a> {
should_exit: bool, data: Vec<(&'a str, u64)>,
temperatures: Vec<u8>, months: [&'a str; 4],
companies: [Company<'a>; 3],
} }
impl App { const TOTAL_REVENUE: &str = "Total Revenue";
fn new() -> Self {
let mut rng = thread_rng(); impl<'a> App<'a> {
let temperatures = (0..24).map(|_| rng.gen_range(50..90)).collect(); fn new() -> App<'a> {
Self { App {
should_exit: false, data: vec![
temperatures, ("B1", 9),
("B2", 12),
("B3", 5),
("B4", 8),
("B5", 2),
("B6", 4),
("B7", 5),
("B8", 9),
("B9", 14),
("B10", 15),
("B11", 1),
("B12", 0),
("B13", 4),
("B14", 6),
("B15", 4),
("B16", 6),
("B17", 4),
("B18", 7),
("B19", 13),
("B20", 8),
("B21", 11),
("B22", 9),
("B23", 3),
("B24", 5),
],
companies: [
Company {
label: "Comp.A",
revenue: [9500, 12500, 5300, 8500],
bar_style: Style::default().fg(Color::Green),
},
Company {
label: "Comp.B",
revenue: [1500, 2500, 3000, 500],
bar_style: Style::default().fg(Color::Yellow),
},
Company {
label: "Comp.C",
revenue: [10500, 10600, 9000, 4200],
bar_style: Style::default().fg(Color::White),
},
],
months: ["Mars", "Apr", "May", "Jun"],
} }
} }
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { fn on_tick(&mut self) {
while !self.should_exit { let value = self.data.pop().unwrap();
terminal.draw(|frame| self.draw(frame))?; self.data.insert(0, value);
self.handle_events()?; }
} }
Ok(())
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::new();
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
} }
fn handle_events(&mut self) -> Result<()> { Ok(())
if let Event::Key(key) = event::read()? { }
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
self.should_exit = true; fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &app))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
} }
} }
Ok(()) if last_tick.elapsed() >= tick_rate {
} app.on_tick();
last_tick = Instant::now();
fn draw(&self, frame: &mut Frame) { }
let [title, vertical, horizontal] = Layout::vertical([
Constraint::Length(1),
Constraint::Fill(1),
Constraint::Fill(1),
])
.spacing(1)
.areas(frame.area());
frame.render_widget("Barchart".bold().into_centered_line(), title);
frame.render_widget(vertical_barchart(&self.temperatures), vertical);
frame.render_widget(horizontal_barchart(&self.temperatures), horizontal);
} }
} }
/// Create a vertical bar chart from the temperatures data. fn ui(frame: &mut Frame, app: &App) {
fn vertical_barchart(temperatures: &[u8]) -> BarChart { let vertical = Layout::vertical([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]);
let bars: Vec<Bar> = temperatures let horizontal = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
.iter() let [top, bottom] = frame.size().split(&vertical);
.enumerate() let [left, right] = bottom.split(&horizontal);
.map(|(hour, value)| vertical_bar(hour, value))
.collect(); let barchart = BarChart::default()
let title = Line::from("Weather (Vertical)").centered(); .block(Block::default().title("Data1").borders(Borders::ALL))
BarChart::default() .data(&app.data)
.data(BarGroup::default().bars(&bars)) .bar_width(9)
.block(Block::new().title(title)) .bar_style(Style::default().fg(Color::Yellow))
.bar_width(5) .value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
frame.render_widget(barchart, top);
draw_bar_with_group_labels(frame, app, left);
draw_horizontal_bars(frame, app, right);
} }
fn vertical_bar(hour: usize, temperature: &u8) -> Bar { fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGroup<'a>> {
Bar::default() app.months
.value(u64::from(*temperature))
.label(Line::from(format!("{hour:>02}:00")))
.text_value(format!("{temperature:>3}°"))
.style(temperature_style(*temperature))
.value_style(temperature_style(*temperature).reversed())
}
/// Create a horizontal bar chart from the temperatures data.
fn horizontal_barchart(temperatures: &[u8]) -> BarChart {
let bars: Vec<Bar> = temperatures
.iter() .iter()
.enumerate() .enumerate()
.map(|(hour, value)| horizontal_bar(hour, value)) .map(|(i, &month)| {
.collect(); let bars: Vec<Bar> = app
let title = Line::from("Weather (Horizontal)").centered(); .companies
BarChart::default() .iter()
.block(Block::new().title(title)) .map(|c| {
.data(BarGroup::default().bars(&bars)) let mut bar = Bar::default()
.value(c.revenue[i])
.style(c.bar_style)
.value_style(
Style::default()
.bg(c.bar_style.fg.unwrap())
.fg(Color::Black),
);
if combine_values_and_labels {
bar = bar.text_value(format!(
"{} ({:.1} M)",
c.label,
(c.revenue[i] as f64) / 1000.
));
} else {
bar = bar
.text_value(format!("{:.1}", (c.revenue[i] as f64) / 1000.))
.label(c.label.into());
}
bar
})
.collect();
BarGroup::default()
.label(Line::from(month).alignment(Alignment::Center))
.bars(&bars)
})
.collect()
}
fn draw_bar_with_group_labels(f: &mut Frame, app: &App, area: Rect) {
let groups = create_groups(app, false);
let mut barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
.bar_width(7)
.group_gap(3);
for group in groups {
barchart = barchart.data(group)
}
f.render_widget(barchart, area);
const LEGEND_HEIGHT: u16 = 6;
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
let legend_width = TOTAL_REVENUE.len() as u16 + 2;
let legend_area = Rect {
height: LEGEND_HEIGHT,
width: legend_width,
y: area.y,
x: area.right() - legend_width,
};
draw_legend(f, legend_area);
}
}
fn draw_horizontal_bars(f: &mut Frame, app: &App, area: Rect) {
let groups = create_groups(app, true);
let mut barchart = BarChart::default()
.block(Block::default().title("Data1").borders(Borders::ALL))
.bar_width(1) .bar_width(1)
.group_gap(1)
.bar_gap(0) .bar_gap(0)
.direction(Direction::Horizontal) .direction(Direction::Horizontal);
for group in groups {
barchart = barchart.data(group)
}
f.render_widget(barchart, area);
const LEGEND_HEIGHT: u16 = 6;
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
let legend_width = TOTAL_REVENUE.len() as u16 + 2;
let legend_area = Rect {
height: LEGEND_HEIGHT,
width: legend_width,
y: area.y,
x: area.right() - legend_width,
};
draw_legend(f, legend_area);
}
} }
fn horizontal_bar(hour: usize, temperature: &u8) -> Bar { fn draw_legend(f: &mut Frame, area: Rect) {
let style = temperature_style(*temperature); let text = vec![
Bar::default() Line::from(Span::styled(
.value(u64::from(*temperature)) TOTAL_REVENUE,
.label(Line::from(format!("{hour:>02}:00"))) Style::default()
.text_value(format!("{temperature:>3}°")) .add_modifier(Modifier::BOLD)
.style(style) .fg(Color::White),
.value_style(style.reversed()) )),
} Line::from(Span::styled(
"- Company A",
Style::default().fg(Color::Green),
)),
Line::from(Span::styled(
"- Company B",
Style::default().fg(Color::Yellow),
)),
Line::from(vec![Span::styled(
"- Company C",
Style::default().fg(Color::White),
)]),
];
/// create a yellow to red value based on the value (50-90) let block = Block::default()
fn temperature_style(value: u8) -> Style { .borders(Borders::ALL)
let green = (255.0 * (1.0 - f64::from(value - 50) / 40.0)) as u8; .style(Style::default().fg(Color::White));
let color = Color::Rgb(255, green, 0); let paragraph = Paragraph::new(text).block(block);
Style::new().fg(color) f.render_widget(paragraph, area);
} }

View File

@@ -3,10 +3,10 @@
Output "target/barchart.gif" Output "target/barchart.gif"
Set Theme "Aardvark Blue" Set Theme "Aardvark Blue"
Set Width 1200 Set Width 1200
Set Height 600 Set Height 800
Hide Hide
Type "cargo run --example=barchart" Type "cargo run --example=barchart"
Enter Enter
Sleep 1s Sleep 1s
Show Show
Sleep 1s Sleep 5s

View File

@@ -1,49 +1,77 @@
//! # [Ratatui] Block example use std::{
//! error::Error,
//! The latest version of this example is available in the [examples] folder in the repository. io::{stdout, Stdout},
//! ops::ControlFlow,
//! Please note that the examples are designed to be run against the `main` branch of the Github time::Duration,
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use color_eyre::Result;
use ratatui::{
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Alignment, Constraint, Layout, Rect},
style::{Style, Stylize},
text::Line,
widgets::{Block, BorderType, Borders, Padding, Paragraph, Wrap},
DefaultTerminal, Frame,
}; };
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use itertools::Itertools;
use ratatui::{
prelude::*,
widgets::{
block::{Position, Title},
Block, BorderType, Borders, Padding, Paragraph, Wrap,
},
};
// These type aliases are used to make the code more readable by reducing repetition of the generic
// types. They are not necessary for the functionality of the code.
type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
type Result<T> = std::result::Result<T, Box<dyn Error>>;
fn main() -> Result<()> { fn main() -> Result<()> {
color_eyre::install()?; let mut terminal = setup_terminal()?;
let terminal = ratatui::init(); let result = run(&mut terminal);
let result = run(terminal); restore_terminal(terminal)?;
ratatui::restore();
result if let Err(err) = result {
eprintln!("{err:?}");
}
Ok(())
} }
fn run(mut terminal: DefaultTerminal) -> Result<()> { fn setup_terminal() -> Result<Terminal> {
enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal(mut terminal: Terminal) -> Result<()> {
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
Ok(())
}
fn run(terminal: &mut Terminal) -> Result<()> {
loop { loop {
terminal.draw(draw)?; terminal.draw(ui)?;
if let Event::Key(key) = event::read()? { if handle_events()?.is_break() {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { return Ok(());
break Ok(());
}
} }
} }
} }
fn draw(frame: &mut Frame) { fn handle_events() -> Result<ControlFlow<()>> {
let (title_area, layout) = calculate_layout(frame.area()); if event::poll(Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(ControlFlow::Break(()));
}
}
}
Ok(ControlFlow::Continue(()))
}
fn ui(frame: &mut Frame) {
let (title_area, layout) = calculate_layout(frame.size());
render_title(frame, title_area); render_title(frame, title_area);
@@ -76,8 +104,8 @@ fn draw(frame: &mut Frame) {
/// Returns a tuple of the title area and the main areas. /// Returns a tuple of the title area and the main areas.
fn calculate_layout(area: Rect) -> (Rect, Vec<Vec<Rect>>) { fn calculate_layout(area: Rect) -> (Rect, Vec<Vec<Rect>>) {
let main_layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]); let main_layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
let block_layout = Layout::vertical([Constraint::Max(4); 9]); let block_layout = &Layout::vertical([Constraint::Max(4); 9]);
let [title_area, main_area] = main_layout.areas(area); let [title_area, main_area] = area.split(&main_layout);
let main_areas = block_layout let main_areas = block_layout
.split(main_area) .split(main_area)
.iter() .iter()
@@ -86,7 +114,7 @@ fn calculate_layout(area: Rect) -> (Rect, Vec<Vec<Rect>>) {
.split(area) .split(area)
.to_vec() .to_vec()
}) })
.collect(); .collect_vec();
(title_area, main_areas) (title_area, main_areas)
} }
@@ -107,7 +135,7 @@ fn placeholder_paragraph() -> Paragraph<'static> {
fn render_borders(paragraph: &Paragraph, border: Borders, frame: &mut Frame, area: Rect) { fn render_borders(paragraph: &Paragraph, border: Borders, frame: &mut Frame, area: Rect) {
let block = Block::new() let block = Block::new()
.borders(border) .borders(border)
.title(format!("Borders::{border:#?}")); .title(format!("Borders::{border:#?}", border = border));
frame.render_widget(paragraph.clone().block(block), area); frame.render_widget(paragraph.clone().block(block), area);
} }
@@ -117,27 +145,32 @@ fn render_border_type(
frame: &mut Frame, frame: &mut Frame,
area: Rect, area: Rect,
) { ) {
let block = Block::bordered() let block = Block::new()
.borders(Borders::ALL)
.border_type(border_type) .border_type(border_type)
.title(format!("BorderType::{border_type:#?}")); .title(format!("BorderType::{border_type:#?}"));
frame.render_widget(paragraph.clone().block(block), area); frame.render_widget(paragraph.clone().block(block), area);
} }
fn render_styled_borders(paragraph: &Paragraph, frame: &mut Frame, area: Rect) { fn render_styled_borders(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::bordered() let block = Block::new()
.borders(Borders::ALL)
.border_style(Style::new().blue().on_white().bold().italic()) .border_style(Style::new().blue().on_white().bold().italic())
.title("Styled borders"); .title("Styled borders");
frame.render_widget(paragraph.clone().block(block), area); frame.render_widget(paragraph.clone().block(block), area);
} }
fn render_styled_block(paragraph: &Paragraph, frame: &mut Frame, area: Rect) { fn render_styled_block(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::bordered() let block = Block::new()
.borders(Borders::ALL)
.style(Style::new().blue().on_white().bold().italic()) .style(Style::new().blue().on_white().bold().italic())
.title("Styled block"); .title("Styled block");
frame.render_widget(paragraph.clone().block(block), area); frame.render_widget(paragraph.clone().block(block), area);
} }
// Note: this currently renders incorrectly, see https://github.com/ratatui-org/ratatui/issues/349
fn render_styled_title(paragraph: &Paragraph, frame: &mut Frame, area: Rect) { fn render_styled_title(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::bordered() let block = Block::new()
.borders(Borders::ALL)
.title("Styled title") .title("Styled title")
.title_style(Style::new().blue().on_white().bold().italic()); .title_style(Style::new().blue().on_white().bold().italic());
frame.render_widget(paragraph.clone().block(block), area); frame.render_widget(paragraph.clone().block(block), area);
@@ -148,38 +181,65 @@ fn render_styled_title_content(paragraph: &Paragraph, frame: &mut Frame, area: R
"Styled ".blue().on_white().bold().italic(), "Styled ".blue().on_white().bold().italic(),
"title content".red().on_white().bold().italic(), "title content".red().on_white().bold().italic(),
]); ]);
let block = Block::bordered().title(title); let block = Block::new().borders(Borders::ALL).title(title);
frame.render_widget(paragraph.clone().block(block), area); frame.render_widget(paragraph.clone().block(block), area);
} }
fn render_multiple_titles(paragraph: &Paragraph, frame: &mut Frame, area: Rect) { fn render_multiple_titles(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::bordered() let block = Block::new()
.borders(Borders::ALL)
.title("Multiple".blue().on_white().bold().italic()) .title("Multiple".blue().on_white().bold().italic())
.title("Titles".red().on_white().bold().italic()); .title("Titles".red().on_white().bold().italic());
frame.render_widget(paragraph.clone().block(block), area); frame.render_widget(paragraph.clone().block(block), area);
} }
fn render_multiple_title_positions(paragraph: &Paragraph, frame: &mut Frame, area: Rect) { fn render_multiple_title_positions(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::bordered() let block = Block::new()
.title(Line::from("top left").left_aligned()) .borders(Borders::ALL)
.title(Line::from("top center").centered()) .title(
.title(Line::from("top right").right_aligned()) Title::from("top left")
.title_bottom(Line::from("bottom left").left_aligned()) .position(Position::Top)
.title_bottom(Line::from("bottom center").centered()) .alignment(Alignment::Left),
.title_bottom(Line::from("bottom right").right_aligned()); )
.title(
Title::from("top center")
.position(Position::Top)
.alignment(Alignment::Center),
)
.title(
Title::from("top right")
.position(Position::Top)
.alignment(Alignment::Right),
)
.title(
Title::from("bottom left")
.position(Position::Bottom)
.alignment(Alignment::Left),
)
.title(
Title::from("bottom center")
.position(Position::Bottom)
.alignment(Alignment::Center),
)
.title(
Title::from("bottom right")
.position(Position::Bottom)
.alignment(Alignment::Right),
);
frame.render_widget(paragraph.clone().block(block), area); frame.render_widget(paragraph.clone().block(block), area);
} }
fn render_padding(paragraph: &Paragraph, frame: &mut Frame, area: Rect) { fn render_padding(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::bordered() let block = Block::new()
.padding(Padding::new(5, 10, 1, 2)) .borders(Borders::ALL)
.title("Padding"); .title("Padding")
.padding(Padding::new(5, 10, 1, 2));
frame.render_widget(paragraph.clone().block(block), area); frame.render_widget(paragraph.clone().block(block), area);
} }
fn render_nested_blocks(paragraph: &Paragraph, frame: &mut Frame, area: Rect) { fn render_nested_blocks(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let outer_block = Block::bordered().title("Outer block"); let outer_block = Block::new().borders(Borders::ALL).title("Outer block");
let inner_block = Block::bordered().title("Inner block"); let inner_block = Block::new().borders(Borders::ALL).title("Inner block");
let inner = outer_block.inner(area); let inner = outer_block.inner(area);
frame.render_widget(outer_block, area); frame.render_widget(outer_block, area);
frame.render_widget(paragraph.clone().block(inner_block), inner); frame.render_widget(paragraph.clone().block(inner_block), inner);

View File

@@ -1,52 +1,49 @@
//! # [Ratatui] Calendar example use std::{error::Error, io};
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use color_eyre::Result; use crossterm::{
use ratatui::{ event::{self, Event, KeyCode},
crossterm::event::{self, Event, KeyCode, KeyEventKind}, execute,
layout::{Constraint, Layout, Margin}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
style::{Color, Modifier, Style},
widgets::calendar::{CalendarEventStore, DateStyler, Monthly},
DefaultTerminal, Frame,
}; };
use ratatui::{prelude::*, widgets::calendar::*};
use time::{Date, Month, OffsetDateTime}; use time::{Date, Month, OffsetDateTime};
fn main() -> Result<()> { fn main() -> Result<(), Box<dyn Error>> {
color_eyre::install()?; enable_raw_mode()?;
let terminal = ratatui::init(); let mut stdout = io::stdout();
let result = run(terminal); execute!(stdout, EnterAlternateScreen)?;
ratatui::restore(); let backend = CrosstermBackend::new(stdout);
result let mut terminal = Terminal::new(backend)?;
}
fn run(mut terminal: DefaultTerminal) -> Result<()> {
loop { loop {
terminal.draw(draw)?; let _ = terminal.draw(draw);
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { #[allow(clippy::single_match)]
break Ok(()); match key.code {
} KeyCode::Char(_) => {
break;
}
_ => {}
};
} }
} }
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
Ok(())
} }
fn draw(frame: &mut Frame) { fn draw(f: &mut Frame) {
let area = frame.area().inner(Margin { let app_area = f.size();
vertical: 1,
horizontal: 1, let calarea = Rect {
}); x: app_area.x + 1,
y: app_area.y + 1,
height: app_area.height - 1,
width: app_area.width - 1,
};
let mut start = OffsetDateTime::now_local() let mut start = OffsetDateTime::now_local()
.unwrap() .unwrap()
@@ -58,7 +55,7 @@ fn draw(frame: &mut Frame) {
let list = make_dates(start.year()); let list = make_dates(start.year());
let rows = Layout::vertical([Constraint::Ratio(1, 3); 3]).split(area); let rows = Layout::vertical([Constraint::Ratio(1, 3); 3]).split(calarea);
let cols = rows.iter().flat_map(|row| { let cols = rows.iter().flat_map(|row| {
Layout::horizontal([Constraint::Ratio(1, 4); 4]) Layout::horizontal([Constraint::Ratio(1, 4); 4])
.split(*row) .split(*row)
@@ -66,7 +63,7 @@ fn draw(frame: &mut Frame) {
}); });
for col in cols { for col in cols {
let cal = cals::get_cal(start.month(), start.year(), &list); let cal = cals::get_cal(start.month(), start.year(), &list);
frame.render_widget(cal, col); f.render_widget(cal, col);
start = start.replace_month(start.month().next()).unwrap(); start = start.replace_month(start.month().next()).unwrap();
} }
} }
@@ -152,16 +149,17 @@ fn make_dates(current_year: i32) -> CalendarEventStore {
} }
mod cals { mod cals {
#[allow(clippy::wildcard_imports)]
use super::*; use super::*;
pub fn get_cal<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> { pub(super) fn get_cal<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
use Month::*;
match m { match m {
Month::May => example1(m, y, es), May => example1(m, y, es),
Month::June => example2(m, y, es), June => example2(m, y, es),
Month::July | Month::December => example3(m, y, es), July => example3(m, y, es),
Month::February => example4(m, y, es), December => example3(m, y, es),
Month::November => example5(m, y, es), February => example4(m, y, es),
November => example5(m, y, es),
_ => default(m, y, es), _ => default(m, y, es),
} }
} }

View File

@@ -1,39 +1,20 @@
//! # [Ratatui] Canvas example use std::{
//! io::{self, stdout, Stdout},
//! The latest version of this example is available in the [examples] folder in the repository. time::{Duration, Instant},
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use std::time::{Duration, Instant};
use color_eyre::Result;
use ratatui::{
crossterm::event::{self, Event, KeyCode},
layout::{Constraint, Layout, Rect},
style::{Color, Stylize},
symbols::Marker,
widgets::{
canvas::{Canvas, Circle, Map, MapResolution, Rectangle},
Block, Widget,
},
DefaultTerminal, Frame,
}; };
fn main() -> Result<()> { use crossterm::{
color_eyre::install()?; event::{self, Event, KeyCode},
let terminal = ratatui::init(); terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
let app_result = App::new().run(terminal); ExecutableCommand,
ratatui::restore(); };
app_result use ratatui::{
prelude::*,
widgets::{canvas::*, *},
};
fn main() -> io::Result<()> {
App::run()
} }
struct App { struct App {
@@ -48,8 +29,8 @@ struct App {
} }
impl App { impl App {
const fn new() -> Self { fn new() -> App {
Self { App {
x: 0.0, x: 0.0,
y: 0.0, y: 0.0,
ball: Circle { ball: Circle {
@@ -66,30 +47,33 @@ impl App {
} }
} }
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { pub fn run() -> io::Result<()> {
let tick_rate = Duration::from_millis(16); let mut terminal = init_terminal()?;
let mut app = App::new();
let mut last_tick = Instant::now(); let mut last_tick = Instant::now();
let tick_rate = Duration::from_millis(16);
loop { loop {
terminal.draw(|frame| self.draw(frame))?; let _ = terminal.draw(|frame| app.ui(frame));
let timeout = tick_rate.saturating_sub(last_tick.elapsed()); let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout)? { if event::poll(timeout)? {
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
match key.code { match key.code {
KeyCode::Char('q') => break Ok(()), KeyCode::Char('q') => break,
KeyCode::Down | KeyCode::Char('j') => self.y += 1.0, KeyCode::Down | KeyCode::Char('j') => app.y += 1.0,
KeyCode::Up | KeyCode::Char('k') => self.y -= 1.0, KeyCode::Up | KeyCode::Char('k') => app.y -= 1.0,
KeyCode::Right | KeyCode::Char('l') => self.x += 1.0, KeyCode::Right | KeyCode::Char('l') => app.x += 1.0,
KeyCode::Left | KeyCode::Char('h') => self.x -= 1.0, KeyCode::Left | KeyCode::Char('h') => app.x -= 1.0,
_ => {} _ => {}
} }
} }
} }
if last_tick.elapsed() >= tick_rate { if last_tick.elapsed() >= tick_rate {
self.on_tick(); app.on_tick();
last_tick = Instant::now(); last_tick = Instant::now();
} }
} }
restore_terminal()
} }
fn on_tick(&mut self) { fn on_tick(&mut self) {
@@ -107,13 +91,13 @@ impl App {
// bounce the ball by flipping the velocity vector // bounce the ball by flipping the velocity vector
let ball = &self.ball; let ball = &self.ball;
let playground = self.playground; let playground = self.playground;
if ball.x - ball.radius < f64::from(playground.left()) if ball.x - ball.radius < playground.left() as f64
|| ball.x + ball.radius > f64::from(playground.right()) || ball.x + ball.radius > playground.right() as f64
{ {
self.vx = -self.vx; self.vx = -self.vx;
} }
if ball.y - ball.radius < f64::from(playground.top()) if ball.y - ball.radius < playground.top() as f64
|| ball.y + ball.radius > f64::from(playground.bottom()) || ball.y + ball.radius > playground.bottom() as f64
{ {
self.vy = -self.vy; self.vy = -self.vy;
} }
@@ -122,12 +106,12 @@ impl App {
self.ball.y += self.vy; self.ball.y += self.vy;
} }
fn draw(&self, frame: &mut Frame) { fn ui(&self, frame: &mut Frame) {
let horizontal = let horizontal =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]); Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]); let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [map, right] = horizontal.areas(frame.area()); let [map, right] = frame.size().split(&horizontal);
let [pong, boxes] = vertical.areas(right); let [pong, boxes] = right.split(&vertical);
frame.render_widget(self.map_canvas(), map); frame.render_widget(self.map_canvas(), map);
frame.render_widget(self.pong_canvas(), pong); frame.render_widget(self.pong_canvas(), pong);
@@ -136,7 +120,7 @@ impl App {
fn map_canvas(&self) -> impl Widget + '_ { fn map_canvas(&self) -> impl Widget + '_ {
Canvas::default() Canvas::default()
.block(Block::bordered().title("World")) .block(Block::default().borders(Borders::ALL).title("World"))
.marker(self.marker) .marker(self.marker)
.paint(|ctx| { .paint(|ctx| {
ctx.draw(&Map { ctx.draw(&Map {
@@ -151,7 +135,7 @@ impl App {
fn pong_canvas(&self) -> impl Widget + '_ { fn pong_canvas(&self) -> impl Widget + '_ {
Canvas::default() Canvas::default()
.block(Block::bordered().title("Pong")) .block(Block::default().borders(Borders::ALL).title("Pong"))
.marker(self.marker) .marker(self.marker)
.paint(|ctx| { .paint(|ctx| {
ctx.draw(&self.ball); ctx.draw(&self.ball);
@@ -161,40 +145,50 @@ impl App {
} }
fn boxes_canvas(&self, area: Rect) -> impl Widget { fn boxes_canvas(&self, area: Rect) -> impl Widget {
let left = 0.0; let (left, right, bottom, top) =
let right = f64::from(area.width); (0.0, area.width as f64, 0.0, area.height as f64 * 2.0 - 4.0);
let bottom = 0.0;
let top = f64::from(area.height).mul_add(2.0, -4.0);
Canvas::default() Canvas::default()
.block(Block::bordered().title("Rects")) .block(Block::default().borders(Borders::ALL).title("Rects"))
.marker(self.marker) .marker(self.marker)
.x_bounds([left, right]) .x_bounds([left, right])
.y_bounds([bottom, top]) .y_bounds([bottom, top])
.paint(|ctx| { .paint(|ctx| {
for i in 0..=11 { for i in 0..=11 {
ctx.draw(&Rectangle { ctx.draw(&Rectangle {
x: f64::from(i * i + 3 * i) / 2.0 + 2.0, x: (i * i + 3 * i) as f64 / 2.0 + 2.0,
y: 2.0, y: 2.0,
width: f64::from(i), width: i as f64,
height: f64::from(i), height: i as f64,
color: Color::Red, color: Color::Red,
}); });
ctx.draw(&Rectangle { ctx.draw(&Rectangle {
x: f64::from(i * i + 3 * i) / 2.0 + 2.0, x: (i * i + 3 * i) as f64 / 2.0 + 2.0,
y: 21.0, y: 21.0,
width: f64::from(i), width: i as f64,
height: f64::from(i), height: i as f64,
color: Color::Blue, color: Color::Blue,
}); });
} }
for i in 0..100 { for i in 0..100 {
if i % 10 != 0 { if i % 10 != 0 {
ctx.print(f64::from(i) + 1.0, 0.0, format!("{i}", i = i % 10)); ctx.print(i as f64 + 1.0, 0.0, format!("{i}", i = i % 10));
} }
if i % 2 == 0 && i % 10 != 0 { if i % 2 == 0 && i % 10 != 0 {
ctx.print(0.0, f64::from(i), format!("{i}", i = i % 10)); ctx.print(0.0, i as f64, format!("{i}", i = i % 10));
} }
} }
}) })
} }
} }
fn init_terminal() -> io::Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
Terminal::new(CrosstermBackend::new(stdout()))
}
fn restore_terminal() -> io::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -1,49 +1,21 @@
//! # [Ratatui] Chart example use std::{
//! error::Error,
//! The latest version of this example is available in the [examples] folder in the repository. io,
//! time::{Duration, Instant},
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use std::time::{Duration, Instant};
use color_eyre::Result;
use ratatui::{
crossterm::event::{self, Event, KeyCode},
layout::{Constraint, Layout, Rect},
style::{Color, Modifier, Style, Stylize},
symbols::{self, Marker},
text::{Line, Span},
widgets::{Axis, Block, Chart, Dataset, GraphType, LegendPosition},
DefaultTerminal, Frame,
}; };
fn main() -> Result<()> { use crossterm::{
color_eyre::install()?; event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
let terminal = ratatui::init(); execute,
let app_result = App::new().run(terminal); terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ratatui::restore(); };
app_result use ratatui::{
} prelude::*,
widgets::{block::Title, *},
struct App { };
signal1: SinSignal,
data1: Vec<(f64, f64)>,
signal2: SinSignal,
data2: Vec<(f64, f64)>,
window: [f64; 2],
}
#[derive(Clone)] #[derive(Clone)]
struct SinSignal { pub struct SinSignal {
x: f64, x: f64,
interval: f64, interval: f64,
period: f64, period: f64,
@@ -51,8 +23,8 @@ struct SinSignal {
} }
impl SinSignal { impl SinSignal {
const fn new(interval: f64, period: f64, scale: f64) -> Self { pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
Self { SinSignal {
x: 0.0, x: 0.0,
interval, interval,
period, period,
@@ -70,13 +42,21 @@ impl Iterator for SinSignal {
} }
} }
struct App {
signal1: SinSignal,
data1: Vec<(f64, f64)>,
signal2: SinSignal,
data2: Vec<(f64, f64)>,
window: [f64; 2],
}
impl App { impl App {
fn new() -> Self { fn new() -> App {
let mut signal1 = SinSignal::new(0.2, 3.0, 18.0); let mut signal1 = SinSignal::new(0.2, 3.0, 18.0);
let mut signal2 = SinSignal::new(0.1, 2.0, 10.0); let mut signal2 = SinSignal::new(0.1, 2.0, 10.0);
let data1 = signal1.by_ref().take(200).collect::<Vec<(f64, f64)>>(); let data1 = signal1.by_ref().take(200).collect::<Vec<(f64, f64)>>();
let data2 = signal2.by_ref().take(200).collect::<Vec<(f64, f64)>>(); let data2 = signal2.by_ref().take(200).collect::<Vec<(f64, f64)>>();
Self { App {
signal1, signal1,
data1, data1,
signal2, signal2,
@@ -85,136 +65,136 @@ impl App {
} }
} }
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
let tick_rate = Duration::from_millis(250);
let mut last_tick = Instant::now();
loop {
terminal.draw(|frame| self.draw(frame))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
self.on_tick();
last_tick = Instant::now();
}
}
}
fn on_tick(&mut self) { fn on_tick(&mut self) {
self.data1.drain(0..5); for _ in 0..5 {
self.data1.remove(0);
}
self.data1.extend(self.signal1.by_ref().take(5)); self.data1.extend(self.signal1.by_ref().take(5));
for _ in 0..10 {
self.data2.drain(0..10); self.data2.remove(0);
}
self.data2.extend(self.signal2.by_ref().take(10)); self.data2.extend(self.signal2.by_ref().take(10));
self.window[0] += 1.0; self.window[0] += 1.0;
self.window[1] += 1.0; self.window[1] += 1.0;
} }
}
fn draw(&self, frame: &mut Frame) { fn main() -> Result<(), Box<dyn Error>> {
let [top, bottom] = Layout::vertical([Constraint::Fill(1); 2]).areas(frame.area()); // setup terminal
let [animated_chart, bar_chart] = enable_raw_mode()?;
Layout::horizontal([Constraint::Fill(1), Constraint::Length(29)]).areas(top); let mut stdout = io::stdout();
let [line_chart, scatter] = Layout::horizontal([Constraint::Fill(1); 2]).areas(bottom); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
self.render_animated_chart(frame, animated_chart); // create app and run it
render_barchart(frame, bar_chart); let tick_rate = Duration::from_millis(250);
render_line_chart(frame, line_chart); let app = App::new();
render_scatter(frame, scatter); let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
} }
fn render_animated_chart(&self, frame: &mut Frame, area: Rect) { Ok(())
let x_labels = vec![ }
Span::styled(
format!("{}", self.window[0]),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(format!("{}", (self.window[0] + self.window[1]) / 2.0)),
Span::styled(
format!("{}", self.window[1]),
Style::default().add_modifier(Modifier::BOLD),
),
];
let datasets = vec![
Dataset::default()
.name("data2")
.marker(symbols::Marker::Dot)
.style(Style::default().fg(Color::Cyan))
.data(&self.data1),
Dataset::default()
.name("data3")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.data(&self.data2),
];
let chart = Chart::new(datasets) fn run_app<B: Backend>(
.block(Block::bordered()) terminal: &mut Terminal<B>,
.x_axis( mut app: App,
Axis::default() tick_rate: Duration,
.title("X Axis") ) -> io::Result<()> {
.style(Style::default().fg(Color::Gray)) let mut last_tick = Instant::now();
.labels(x_labels) loop {
.bounds(self.window), terminal.draw(|f| ui(f, &app))?;
)
.y_axis(
Axis::default()
.title("Y Axis")
.style(Style::default().fg(Color::Gray))
.labels(["-20".bold(), "0".into(), "20".bold()])
.bounds([-20.0, 20.0]),
);
frame.render_widget(chart, area); let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
} }
} }
fn render_barchart(frame: &mut Frame, bar_chart: Rect) { fn ui(frame: &mut Frame, app: &App) {
let dataset = Dataset::default() let area = frame.size();
.marker(symbols::Marker::HalfBlock)
.style(Style::new().fg(Color::Blue))
.graph_type(GraphType::Bar)
// a bell curve
.data(&[
(0., 0.4),
(10., 2.9),
(20., 13.5),
(30., 41.1),
(40., 80.1),
(50., 100.0),
(60., 80.1),
(70., 41.1),
(80., 13.5),
(90., 2.9),
(100., 0.4),
]);
let chart = Chart::new(vec![dataset]) let vertical = Layout::vertical([Constraint::Percentage(40), Constraint::Percentage(60)]);
.block(Block::bordered().title_top(Line::from("Bar chart").cyan().bold().centered())) let horizontal = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
let [chart1, bottom] = area.split(&vertical);
let [line_chart, scatter] = bottom.split(&horizontal);
render_chart1(frame, chart1, app);
render_line_chart(frame, line_chart);
render_scatter(frame, scatter);
}
fn render_chart1(f: &mut Frame, area: Rect, app: &App) {
let x_labels = vec![
Span::styled(
format!("{}", app.window[0]),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(format!("{}", (app.window[0] + app.window[1]) / 2.0)),
Span::styled(
format!("{}", app.window[1]),
Style::default().add_modifier(Modifier::BOLD),
),
];
let datasets = vec![
Dataset::default()
.name("data2")
.marker(symbols::Marker::Dot)
.style(Style::default().fg(Color::Cyan))
.data(&app.data1),
Dataset::default()
.name("data3")
.marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow))
.data(&app.data2),
];
let chart = Chart::new(datasets)
.block(
Block::default()
.title("Chart 1".cyan().bold())
.borders(Borders::ALL),
)
.x_axis( .x_axis(
Axis::default() Axis::default()
.style(Style::default().gray()) .title("X Axis")
.bounds([0.0, 100.0]) .style(Style::default().fg(Color::Gray))
.labels(["0".bold(), "50".into(), "100.0".bold()]), .labels(x_labels)
.bounds(app.window),
) )
.y_axis( .y_axis(
Axis::default() Axis::default()
.style(Style::default().gray()) .title("Y Axis")
.bounds([0.0, 100.0]) .style(Style::default().fg(Color::Gray))
.labels(["0".bold(), "50".into(), "100.0".bold()]), .labels(vec!["-20".bold(), "0".into(), "20".bold()])
) .bounds([-20.0, 20.0]),
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2))); );
frame.render_widget(chart, bar_chart); f.render_widget(chart, area);
} }
fn render_line_chart(frame: &mut Frame, area: Rect) { fn render_line_chart(f: &mut Frame, area: Rect) {
let datasets = vec![Dataset::default() let datasets = vec![Dataset::default()
.name("Line from only 2 points".italic()) .name("Line from only 2 points".italic())
.marker(symbols::Marker::Braille) .marker(symbols::Marker::Braille)
@@ -223,28 +203,36 @@ fn render_line_chart(frame: &mut Frame, area: Rect) {
.data(&[(1., 1.), (4., 4.)])]; .data(&[(1., 1.), (4., 4.)])];
let chart = Chart::new(datasets) let chart = Chart::new(datasets)
.block(Block::bordered().title(Line::from("Line chart").cyan().bold().centered())) .block(
Block::default()
.title(
Title::default()
.content("Line chart".cyan().bold())
.alignment(Alignment::Center),
)
.borders(Borders::ALL),
)
.x_axis( .x_axis(
Axis::default() Axis::default()
.title("X Axis") .title("X Axis")
.style(Style::default().gray()) .style(Style::default().gray())
.bounds([0.0, 5.0]) .bounds([0.0, 5.0])
.labels(["0".bold(), "2.5".into(), "5.0".bold()]), .labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
) )
.y_axis( .y_axis(
Axis::default() Axis::default()
.title("Y Axis") .title("Y Axis")
.style(Style::default().gray()) .style(Style::default().gray())
.bounds([0.0, 5.0]) .bounds([0.0, 5.0])
.labels(["0".bold(), "2.5".into(), "5.0".bold()]), .labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
) )
.legend_position(Some(LegendPosition::TopLeft)) .legend_position(Some(LegendPosition::TopLeft))
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2))); .hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
frame.render_widget(chart, area); f.render_widget(chart, area)
} }
fn render_scatter(frame: &mut Frame, area: Rect) { fn render_scatter(f: &mut Frame, area: Rect) {
let datasets = vec![ let datasets = vec![
Dataset::default() Dataset::default()
.name("Heavy") .name("Heavy")
@@ -267,24 +255,30 @@ fn render_scatter(frame: &mut Frame, area: Rect) {
]; ];
let chart = Chart::new(datasets) let chart = Chart::new(datasets)
.block(Block::bordered().title(Line::from("Scatter chart").cyan().bold().centered())) .block(
Block::new().borders(Borders::all()).title(
Title::default()
.content("Scatter chart".cyan().bold())
.alignment(Alignment::Center),
),
)
.x_axis( .x_axis(
Axis::default() Axis::default()
.title("Year") .title("Year")
.bounds([1960., 2020.]) .bounds([1960., 2020.])
.style(Style::default().fg(Color::Gray)) .style(Style::default().fg(Color::Gray))
.labels(["1960", "1990", "2020"]), .labels(vec!["1960".into(), "1990".into(), "2020".into()]),
) )
.y_axis( .y_axis(
Axis::default() Axis::default()
.title("Cost") .title("Cost")
.bounds([0., 75000.]) .bounds([0., 75000.])
.style(Style::default().fg(Color::Gray)) .style(Style::default().fg(Color::Gray))
.labels(["0", "37 500", "75 000"]), .labels(vec!["0".into(), "37 500".into(), "75 000".into()]),
) )
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2))); .hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
frame.render_widget(chart, area); f.render_widget(chart, area);
} }
// Data from https://ourworldindata.org/space-exploration-satellites // Data from https://ourworldindata.org/space-exploration-satellites
@@ -303,7 +297,7 @@ const HEAVY_PAYLOAD_DATA: [(f64, f64); 9] = [
const MEDIUM_PAYLOAD_DATA: [(f64, f64); 29] = [ const MEDIUM_PAYLOAD_DATA: [(f64, f64); 29] = [
(1963., 29500.), (1963., 29500.),
(1964., 30600.), (1964., 30600.),
(1965., 177_900.), (1965., 177900.),
(1965., 21000.), (1965., 21000.),
(1966., 17900.), (1966., 17900.),
(1966., 8400.), (1966., 8400.),
@@ -333,7 +327,7 @@ const MEDIUM_PAYLOAD_DATA: [(f64, f64); 29] = [
]; ];
const SMALL_PAYLOAD_DATA: [(f64, f64); 23] = [ const SMALL_PAYLOAD_DATA: [(f64, f64); 23] = [
(1961., 118_500.), (1961., 118500.),
(1962., 14900.), (1962., 14900.),
(1975., 21400.), (1975., 21400.),
(1980., 32800.), (1980., 32800.),

View File

@@ -1,58 +1,53 @@
//! # [Ratatui] Colors example /// This example shows all the colors supported by ratatui. It will render a grid of foreground
//! /// and background colors with their names and indexes.
//! The latest version of this example is available in the [examples] folder in the repository. use std::{
//! error::Error,
//! Please note that the examples are designed to be run against the `main` branch of the Github io::{self, Stdout},
//! repository. This means that you may not be able to compile with the latest release version on result,
//! crates.io, or the one that you have installed locally. time::Duration,
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
// This example shows all the colors supported by ratatui. It will render a grid of foreground
// and background colors with their names and indexes.
use color_eyre::Result;
use itertools::Itertools;
use ratatui::{
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Alignment, Constraint, Layout, Rect},
style::{Color, Style, Stylize},
text::Line,
widgets::{Block, Borders, Paragraph},
DefaultTerminal, Frame,
}; };
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
type Result<T> = result::Result<T, Box<dyn Error>>;
fn main() -> Result<()> { fn main() -> Result<()> {
color_eyre::install()?; let mut terminal = setup_terminal()?;
let terminal = ratatui::init(); let res = run_app(&mut terminal);
let app_result = run(terminal); restore_terminal(terminal)?;
ratatui::restore(); if let Err(err) = res {
app_result eprintln!("{err:?}");
}
Ok(())
} }
fn run(mut terminal: DefaultTerminal) -> Result<()> { fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
loop { loop {
terminal.draw(draw)?; terminal.draw(ui)?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { if event::poll(Duration::from_millis(250))? {
return Ok(()); if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
} }
} }
} }
} }
fn draw(frame: &mut Frame) { fn ui(frame: &mut Frame) {
let layout = Layout::vertical([ let layout = Layout::vertical([
Constraint::Length(30), Constraint::Length(30),
Constraint::Length(17), Constraint::Length(17),
Constraint::Length(2), Constraint::Length(2),
]) ])
.split(frame.area()); .split(frame.size());
render_named_colors(frame, layout[0]); render_named_colors(frame, layout[0]);
render_indexed_colors(frame, layout[1]); render_indexed_colors(frame, layout[1]);
@@ -99,16 +94,19 @@ fn render_fg_named_colors(frame: &mut Frame, bg: Color, area: Rect) {
let inner = block.inner(area); let inner = block.inner(area);
frame.render_widget(block, area); frame.render_widget(block, area);
let vertical = Layout::vertical([Constraint::Length(1); 2]).split(inner); let layout = Layout::vertical([Constraint::Length(1); 2])
let areas = vertical.iter().flat_map(|area| { .split(inner)
Layout::horizontal([Constraint::Ratio(1, 8); 8]) .iter()
.split(*area) .flat_map(|area| {
.to_vec() Layout::horizontal([Constraint::Ratio(1, 8); 8])
}); .split(*area)
for (fg, area) in NAMED_COLORS.into_iter().zip(areas) { .to_vec()
})
.collect_vec();
for (i, &fg) in NAMED_COLORS.iter().enumerate() {
let color_name = fg.to_string(); let color_name = fg.to_string();
let paragraph = Paragraph::new(color_name).fg(fg).bg(bg); let paragraph = Paragraph::new(color_name).fg(fg).bg(bg);
frame.render_widget(paragraph, area); frame.render_widget(paragraph, layout[i]);
} }
} }
@@ -117,16 +115,19 @@ fn render_bg_named_colors(frame: &mut Frame, fg: Color, area: Rect) {
let inner = block.inner(area); let inner = block.inner(area);
frame.render_widget(block, area); frame.render_widget(block, area);
let vertical = Layout::vertical([Constraint::Length(1); 2]).split(inner); let layout = Layout::vertical([Constraint::Length(1); 2])
let areas = vertical.iter().flat_map(|area| { .split(inner)
Layout::horizontal([Constraint::Ratio(1, 8); 8]) .iter()
.split(*area) .flat_map(|area| {
.to_vec() Layout::horizontal([Constraint::Ratio(1, 8); 8])
}); .split(*area)
for (bg, area) in NAMED_COLORS.into_iter().zip(areas) { .to_vec()
})
.collect_vec();
for (i, &bg) in NAMED_COLORS.iter().enumerate() {
let color_name = bg.to_string(); let color_name = bg.to_string();
let paragraph = Paragraph::new(color_name).fg(fg).bg(bg); let paragraph = Paragraph::new(color_name).fg(fg).bg(bg);
frame.render_widget(paragraph, area); frame.render_widget(paragraph, layout[i]);
} }
} }
@@ -210,12 +211,12 @@ fn render_indexed_colors(frame: &mut Frame, area: Rect) {
} }
fn title_block(title: String) -> Block<'static> { fn title_block(title: String) -> Block<'static> {
Block::new() Block::default()
.borders(Borders::TOP) .borders(Borders::TOP)
.title_alignment(Alignment::Center)
.border_style(Style::new().dark_gray()) .border_style(Style::new().dark_gray())
.title_style(Style::new().reset())
.title(title) .title(title)
.title_alignment(Alignment::Center)
.title_style(Style::new().reset())
} }
fn render_indexed_grayscale(frame: &mut Frame, area: Rect) { fn render_indexed_grayscale(frame: &mut Frame, area: Rect) {
@@ -247,3 +248,20 @@ fn render_indexed_grayscale(frame: &mut Frame, area: Rect) {
frame.render_widget(paragraph, layout[i as usize - 232]); frame.render_widget(paragraph, layout[i as usize - 232]);
} }
} }
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
Ok(terminal)
}
fn restore_terminal(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
Ok(())
}

View File

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

View File

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

View File

@@ -1,41 +1,20 @@
//! # [Ratatui] Constraints example use std::io::{self, stdout};
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use color_eyre::Result; use color_eyre::{config::HookBuilder, Result};
use ratatui::{ use crossterm::{
buffer::Buffer, event::{self, Event, KeyCode},
crossterm::event::{self, Event, KeyCode, KeyEventKind}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
layout::{ ExecutableCommand,
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
Layout, Rect,
},
style::{palette::tailwind, Color, Modifier, Style, Stylize},
symbols,
text::Line,
widgets::{
Block, Padding, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget,
Tabs, Widget,
},
DefaultTerminal,
}; };
use ratatui::{layout::Constraint::*, prelude::*, style::palette::tailwind, widgets::*};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator}; use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
const SPACER_HEIGHT: u16 = 0; const SPACER_HEIGHT: u16 = 0;
const ILLUSTRATION_HEIGHT: u16 = 4; const ILLUSTRATION_HEIGHT: u16 = 4;
const EXAMPLE_HEIGHT: u16 = ILLUSTRATION_HEIGHT + SPACER_HEIGHT; const EXAMPLE_HEIGHT: u16 = ILLUSTRATION_HEIGHT + SPACER_HEIGHT;
// priority 1
const FIXED_COLOR: Color = tailwind::RED.c900;
// priority 2 // priority 2
const MIN_COLOR: Color = tailwind::BLUE.c900; const MIN_COLOR: Color = tailwind::BLUE.c900;
const MAX_COLOR: Color = tailwind::BLUE.c800; const MAX_COLOR: Color = tailwind::BLUE.c800;
@@ -44,15 +23,7 @@ const LENGTH_COLOR: Color = tailwind::SLATE.c700;
const PERCENTAGE_COLOR: Color = tailwind::SLATE.c800; const PERCENTAGE_COLOR: Color = tailwind::SLATE.c800;
const RATIO_COLOR: Color = tailwind::SLATE.c900; const RATIO_COLOR: Color = tailwind::SLATE.c900;
// priority 4 // priority 4
const FILL_COLOR: Color = tailwind::SLATE.c950; const PROPORTIONAL_COLOR: Color = tailwind::SLATE.c950;
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::default().run(terminal);
ratatui::restore();
app_result
}
#[derive(Default, Clone, Copy)] #[derive(Default, Clone, Copy)]
struct App { struct App {
@@ -68,12 +39,13 @@ struct App {
#[derive(Default, Debug, Copy, Clone, Display, FromRepr, EnumIter, PartialEq, Eq)] #[derive(Default, Debug, Copy, Clone, Display, FromRepr, EnumIter, PartialEq, Eq)]
enum SelectedTab { enum SelectedTab {
#[default] #[default]
Fixed,
Min, Min,
Max, Max,
Length, Length,
Percentage, Percentage,
Ratio, Ratio,
Fill, Proportional,
} }
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
@@ -83,11 +55,25 @@ enum AppState {
Quit, Quit,
} }
fn main() -> Result<()> {
init_error_hooks()?;
let terminal = init_terminal()?;
// increase the cache size to avoid flickering for indeterminate layouts
Layout::init_cache(100);
App::default().run(terminal)?;
restore_terminal()?;
Ok(())
}
impl App { impl App {
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
self.update_max_scroll_offset(); self.update_max_scroll_offset();
while self.is_running() { while self.is_running() {
terminal.draw(|frame| frame.render_widget(self, frame.area()))?; self.draw(&mut terminal)?;
self.handle_events()?; self.handle_events()?;
} }
Ok(()) Ok(())
@@ -97,23 +83,26 @@ impl App {
self.max_scroll_offset = (self.selected_tab.get_example_count() - 1) * EXAMPLE_HEIGHT; self.max_scroll_offset = (self.selected_tab.get_example_count() - 1) * EXAMPLE_HEIGHT;
} }
fn is_running(self) -> bool { fn is_running(&self) -> bool {
self.state == AppState::Running self.state == AppState::Running
} }
fn draw(self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
Ok(())
}
fn handle_events(&mut self) -> Result<()> { fn handle_events(&mut self) -> Result<()> {
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
if key.kind != KeyEventKind::Press { use KeyCode::*;
return Ok(());
}
match key.code { match key.code {
KeyCode::Char('q') | KeyCode::Esc => self.quit(), Char('q') | Esc => self.quit(),
KeyCode::Char('l') | KeyCode::Right => self.next(), Char('l') | Right => self.next(),
KeyCode::Char('h') | KeyCode::Left => self.previous(), Char('h') | Left => self.previous(),
KeyCode::Char('j') | KeyCode::Down => self.down(), Char('j') | Down => self.down(),
KeyCode::Char('k') | KeyCode::Up => self.up(), Char('k') | Up => self.up(),
KeyCode::Char('g') | KeyCode::Home => self.top(), Char('g') | Home => self.top(),
KeyCode::Char('G') | KeyCode::End => self.bottom(), Char('G') | End => self.bottom(),
_ => (), _ => (),
} }
} }
@@ -137,14 +126,14 @@ impl App {
} }
fn up(&mut self) { fn up(&mut self) {
self.scroll_offset = self.scroll_offset.saturating_sub(1); self.scroll_offset = self.scroll_offset.saturating_sub(1)
} }
fn down(&mut self) { fn down(&mut self) {
self.scroll_offset = self self.scroll_offset = self
.scroll_offset .scroll_offset
.saturating_add(1) .saturating_add(1)
.min(self.max_scroll_offset); .min(self.max_scroll_offset)
} }
fn top(&mut self) { fn top(&mut self) {
@@ -158,16 +147,20 @@ impl App {
impl Widget for App { impl Widget for App {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let [tabs, axis, demo] = Layout::vertical([Length(3), Length(3), Fill(0)]).areas(area); let [tabs, axis, demo] = area.split(&Layout::vertical([
Constraint::Fixed(3),
Constraint::Fixed(3),
Proportional(0),
]));
self.render_tabs(tabs, buf); self.render_tabs(tabs, buf);
Self::render_axis(axis, buf); self.render_axis(axis, buf);
self.render_demo(demo, buf); self.render_demo(demo, buf);
} }
} }
impl App { impl App {
fn render_tabs(self, area: Rect, buf: &mut Buffer) { fn render_tabs(&self, area: Rect, buf: &mut Buffer) {
let titles = SelectedTab::iter().map(SelectedTab::to_tab_title); let titles = SelectedTab::iter().map(SelectedTab::to_tab_title);
let block = Block::new() let block = Block::new()
.title("Constraints ".bold()) .title("Constraints ".bold())
@@ -181,17 +174,17 @@ impl App {
.render(area, buf); .render(area, buf);
} }
fn render_axis(area: Rect, buf: &mut Buffer) { fn render_axis(&self, area: Rect, buf: &mut Buffer) {
let width = area.width as usize; let width = area.width as usize;
// a bar like `<----- 80 px ----->` // a bar like `<----- 80 px ----->`
let width_label = format!("{width} px"); let width_label = format!("{} px", width);
let width_bar = format!( let width_bar = format!(
"<{width_label:-^width$}>", "<{width_label:-^width$}>",
width = width - width_label.len() / 2 width = width - width_label.len() / 2
); );
Paragraph::new(width_bar.dark_gray()) Paragraph::new(width_bar.dark_gray())
.centered() .alignment(Alignment::Center)
.block(Block::new().padding(Padding { .block(Block::default().padding(Padding {
left: 0, left: 0,
right: 0, right: 0,
top: 1, top: 1,
@@ -204,8 +197,7 @@ impl App {
/// ///
/// This function renders the demo content into a separate buffer and then splices the buffer /// This function renders the demo content into a separate buffer and then splices the buffer
/// into the main buffer. This is done to make it possible to handle scrolling easily. /// into the main buffer. This is done to make it possible to handle scrolling easily.
#[allow(clippy::cast_possible_truncation)] fn render_demo(&self, area: Rect, buf: &mut Buffer) {
fn render_demo(self, area: Rect, buf: &mut Buffer) {
// render demo content into a separate buffer so all examples fit we add an extra // render demo content into a separate buffer so all examples fit we add an extra
// area.height to make sure the last example is fully visible even when the scroll offset is // area.height to make sure the last example is fully visible even when the scroll offset is
// at the max // at the max
@@ -232,7 +224,7 @@ impl App {
for (i, cell) in visible_content.enumerate() { for (i, cell) in visible_content.enumerate() {
let x = i as u16 % area.width; let x = i as u16 % area.width;
let y = i as u16 / area.width; let y = i as u16 / area.width;
buf[(area.x + x, area.y + y)] = cell; *buf.get_mut(area.x + x, area.y + y) = cell;
} }
if scrollbar_needed { if scrollbar_needed {
@@ -245,40 +237,43 @@ impl App {
impl SelectedTab { impl SelectedTab {
/// Get the previous tab, if there is no previous tab return the current tab. /// Get the previous tab, if there is no previous tab return the current tab.
fn previous(self) -> Self { fn previous(&self) -> Self {
let current_index: usize = self as usize; let current_index: usize = *self as usize;
let previous_index = current_index.saturating_sub(1); let previous_index = current_index.saturating_sub(1);
Self::from_repr(previous_index).unwrap_or(self) Self::from_repr(previous_index).unwrap_or(*self)
} }
/// Get the next tab, if there is no next tab return the current tab. /// Get the next tab, if there is no next tab return the current tab.
fn next(self) -> Self { fn next(&self) -> Self {
let current_index = self as usize; let current_index = *self as usize;
let next_index = current_index.saturating_add(1); let next_index = current_index.saturating_add(1);
Self::from_repr(next_index).unwrap_or(self) Self::from_repr(next_index).unwrap_or(*self)
} }
const fn get_example_count(self) -> u16 { fn get_example_count(&self) -> u16 {
#[allow(clippy::match_same_arms)] use SelectedTab::*;
match self { match self {
Self::Length => 4, Fixed => 4,
Self::Percentage => 5, Length => 4,
Self::Ratio => 4, Percentage => 5,
Self::Fill => 2, Ratio => 4,
Self::Min => 5, Proportional => 2,
Self::Max => 5, Min => 5,
Max => 5,
} }
} }
fn to_tab_title(value: Self) -> Line<'static> { fn to_tab_title(value: SelectedTab) -> Line<'static> {
use SelectedTab::*;
let text = format!(" {value} "); let text = format!(" {value} ");
let color = match value { let color = match value {
Self::Length => LENGTH_COLOR, Fixed => FIXED_COLOR,
Self::Percentage => PERCENTAGE_COLOR, Length => LENGTH_COLOR,
Self::Ratio => RATIO_COLOR, Percentage => PERCENTAGE_COLOR,
Self::Fill => FILL_COLOR, Ratio => RATIO_COLOR,
Self::Min => MIN_COLOR, Proportional => PROPORTIONAL_COLOR,
Self::Max => MAX_COLOR, Min => MIN_COLOR,
Max => MAX_COLOR,
}; };
text.fg(tailwind::SLATE.c200).bg(color).into() text.fg(tailwind::SLATE.c200).bg(color).into()
} }
@@ -287,40 +282,59 @@ impl SelectedTab {
impl Widget for SelectedTab { impl Widget for SelectedTab {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
match self { match self {
Self::Length => Self::render_length_example(area, buf), SelectedTab::Fixed => self.render_fixed_example(area, buf),
Self::Percentage => Self::render_percentage_example(area, buf), SelectedTab::Length => self.render_length_example(area, buf),
Self::Ratio => Self::render_ratio_example(area, buf), SelectedTab::Percentage => self.render_percentage_example(area, buf),
Self::Fill => Self::render_fill_example(area, buf), SelectedTab::Ratio => self.render_ratio_example(area, buf),
Self::Min => Self::render_min_example(area, buf), SelectedTab::Proportional => self.render_proportional_example(area, buf),
Self::Max => Self::render_max_example(area, buf), SelectedTab::Min => self.render_min_example(area, buf),
SelectedTab::Max => self.render_max_example(area, buf),
} }
} }
} }
impl SelectedTab { impl SelectedTab {
fn render_length_example(area: Rect, buf: &mut Buffer) { fn render_fixed_example(&self, area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, _] = let [example1, example2, example3, example4, _] =
Layout::vertical([Length(EXAMPLE_HEIGHT); 4]).areas(area); area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 5]));
Example::new(&[Length(20), Length(20)]).render(example1, buf); Example::new(&[Fixed(40), Proportional(0)]).render(example1, buf);
Example::new(&[Length(20), Min(20)]).render(example2, buf); Example::new(&[Fixed(20), Fixed(20), Proportional(0)]).render(example2, buf);
Example::new(&[Length(20), Max(20)]).render(example3, buf); Example::new(&[Fixed(20), Min(20), Max(20)]).render(example3, buf);
Example::new(&[
Length(20),
Percentage(20),
Ratio(1, 5),
Proportional(1),
Fixed(15),
])
.render(example4, buf);
} }
fn render_percentage_example(area: Rect, buf: &mut Buffer) { fn render_length_example(&self, area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, example5, _] = let [example1, example2, example3, example4, _] =
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area); area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 5]));
Example::new(&[Percentage(75), Fill(0)]).render(example1, buf); Example::new(&[Length(20), Fixed(20)]).render(example1, buf);
Example::new(&[Percentage(25), Fill(0)]).render(example2, buf); Example::new(&[Length(20), Length(20)]).render(example2, buf);
Example::new(&[Length(20), Min(20)]).render(example3, buf);
Example::new(&[Length(20), Max(20)]).render(example4, buf);
}
fn render_percentage_example(&self, area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, example5, _] =
area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 6]));
Example::new(&[Percentage(75), Proportional(0)]).render(example1, buf);
Example::new(&[Percentage(25), Proportional(0)]).render(example2, buf);
Example::new(&[Percentage(50), Min(20)]).render(example3, buf); Example::new(&[Percentage(50), Min(20)]).render(example3, buf);
Example::new(&[Percentage(0), Max(0)]).render(example4, buf); Example::new(&[Percentage(0), Max(0)]).render(example4, buf);
Example::new(&[Percentage(0), Fill(0)]).render(example5, buf); Example::new(&[Percentage(0), Proportional(0)]).render(example5, buf);
} }
fn render_ratio_example(area: Rect, buf: &mut Buffer) { fn render_ratio_example(&self, area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, _] = let [example1, example2, example3, example4, _] =
Layout::vertical([Length(EXAMPLE_HEIGHT); 5]).areas(area); area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 5]));
Example::new(&[Ratio(1, 2); 2]).render(example1, buf); Example::new(&[Ratio(1, 2); 2]).render(example1, buf);
Example::new(&[Ratio(1, 4); 4]).render(example2, buf); Example::new(&[Ratio(1, 4); 4]).render(example2, buf);
@@ -328,16 +342,16 @@ impl SelectedTab {
Example::new(&[Ratio(1, 2), Percentage(25), Length(10)]).render(example4, buf); Example::new(&[Ratio(1, 2), Percentage(25), Length(10)]).render(example4, buf);
} }
fn render_fill_example(area: Rect, buf: &mut Buffer) { fn render_proportional_example(&self, area: Rect, buf: &mut Buffer) {
let [example1, example2, _] = Layout::vertical([Length(EXAMPLE_HEIGHT); 3]).areas(area); let [example1, example2, _] = area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 3]));
Example::new(&[Fill(1), Fill(2), Fill(3)]).render(example1, buf); Example::new(&[Proportional(1), Proportional(2), Proportional(3)]).render(example1, buf);
Example::new(&[Fill(1), Percentage(50), Fill(1)]).render(example2, buf); Example::new(&[Proportional(1), Percentage(50), Proportional(1)]).render(example2, buf);
} }
fn render_min_example(area: Rect, buf: &mut Buffer) { fn render_min_example(&self, area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, example5, _] = let [example1, example2, example3, example4, example5, _] =
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area); area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 6]));
Example::new(&[Percentage(100), Min(0)]).render(example1, buf); Example::new(&[Percentage(100), Min(0)]).render(example1, buf);
Example::new(&[Percentage(100), Min(20)]).render(example2, buf); Example::new(&[Percentage(100), Min(20)]).render(example2, buf);
@@ -346,9 +360,9 @@ impl SelectedTab {
Example::new(&[Percentage(100), Min(80)]).render(example5, buf); Example::new(&[Percentage(100), Min(80)]).render(example5, buf);
} }
fn render_max_example(area: Rect, buf: &mut Buffer) { fn render_max_example(&self, area: Rect, buf: &mut Buffer) {
let [example1, example2, example3, example4, example5, _] = let [example1, example2, example3, example4, example5, _] =
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area); area.split(&Layout::vertical([Fixed(EXAMPLE_HEIGHT); 6]));
Example::new(&[Percentage(0), Max(0)]).render(example1, buf); Example::new(&[Percentage(0), Max(0)]).render(example1, buf);
Example::new(&[Percentage(0), Max(20)]).render(example2, buf); Example::new(&[Percentage(0), Max(20)]).render(example2, buf);
@@ -372,23 +386,27 @@ impl Example {
impl Widget for Example { impl Widget for Example {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let [area, _] = let [area, _] = area.split(&Layout::vertical([
Layout::vertical([Length(ILLUSTRATION_HEIGHT), Length(SPACER_HEIGHT)]).areas(area); Fixed(ILLUSTRATION_HEIGHT),
Fixed(SPACER_HEIGHT),
]));
let blocks = Layout::horizontal(&self.constraints).split(area); let blocks = Layout::horizontal(&self.constraints).split(area);
for (block, constraint) in blocks.iter().zip(&self.constraints) { for (block, constraint) in blocks.iter().zip(&self.constraints) {
Self::illustration(*constraint, block.width).render(*block, buf); self.illustration(*constraint, block.width)
.render(*block, buf);
} }
} }
} }
impl Example { impl Example {
fn illustration(constraint: Constraint, width: u16) -> impl Widget { fn illustration(&self, constraint: Constraint, width: u16) -> Paragraph {
let color = match constraint { let color = match constraint {
Constraint::Fixed(_) => FIXED_COLOR,
Constraint::Length(_) => LENGTH_COLOR, Constraint::Length(_) => LENGTH_COLOR,
Constraint::Percentage(_) => PERCENTAGE_COLOR, Constraint::Percentage(_) => PERCENTAGE_COLOR,
Constraint::Ratio(_, _) => RATIO_COLOR, Constraint::Ratio(_, _) => RATIO_COLOR,
Constraint::Fill(_) => FILL_COLOR, Constraint::Proportional(_) => PROPORTIONAL_COLOR,
Constraint::Min(_) => MIN_COLOR, Constraint::Min(_) => MIN_COLOR,
Constraint::Max(_) => MAX_COLOR, Constraint::Max(_) => MAX_COLOR,
}; };
@@ -400,6 +418,37 @@ impl Example {
.border_set(symbols::border::QUADRANT_OUTSIDE) .border_set(symbols::border::QUADRANT_OUTSIDE)
.border_style(Style::reset().fg(color).reversed()) .border_style(Style::reset().fg(color).reversed())
.style(Style::default().fg(fg).bg(color)); .style(Style::default().fg(fg).bg(color));
Paragraph::new(text).centered().block(block) Paragraph::new(text)
.alignment(Alignment::Center)
.block(block)
} }
} }
fn init_error_hooks() -> Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info)
}));
Ok(())
}
fn init_terminal() -> Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal() -> Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -1,48 +1,14 @@
//! # [Ratatui] Custom Widget example use std::{error::Error, io, ops::ControlFlow, time::Duration};
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use std::{io::stdout, ops::ControlFlow, time::Duration}; use crossterm::{
event::{
use color_eyre::Result; self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseButton, MouseEvent,
use ratatui::{ MouseEventKind,
buffer::Buffer,
crossterm::{
event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseButton, MouseEvent,
MouseEventKind,
},
execute,
}, },
layout::{Constraint, Layout, Rect}, execute,
style::{Color, Style}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
text::Line,
widgets::{Paragraph, Widget},
DefaultTerminal, Frame,
}; };
use ratatui::{prelude::*, widgets::*};
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
execute!(stdout(), EnableMouseCapture)?;
let app_result = run(terminal);
ratatui::restore();
if let Err(err) = execute!(stdout(), DisableMouseCapture) {
eprintln!("Error disabling mouse capture: {err}");
}
app_result
}
/// A custom widget that renders a button with a label, theme and state. /// A custom widget that renders a button with a label, theme and state.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -90,7 +56,7 @@ const GREEN: Theme = Theme {
/// A button with a label that can be themed. /// A button with a label that can be themed.
impl<'a> Button<'a> { impl<'a> Button<'a> {
pub fn new<T: Into<Line<'a>>>(label: T) -> Self { pub fn new<T: Into<Line<'a>>>(label: T) -> Button<'a> {
Button { Button {
label: label.into(), label: label.into(),
theme: BLUE, theme: BLUE,
@@ -98,19 +64,18 @@ impl<'a> Button<'a> {
} }
} }
pub const fn theme(mut self, theme: Theme) -> Self { pub fn theme(mut self, theme: Theme) -> Button<'a> {
self.theme = theme; self.theme = theme;
self self
} }
pub const fn state(mut self, state: State) -> Self { pub fn state(mut self, state: State) -> Button<'a> {
self.state = state; self.state = state;
self self
} }
} }
impl<'a> Widget for Button<'a> { impl<'a> Widget for Button<'a> {
#[allow(clippy::cast_possible_truncation)]
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let (background, text, shadow, highlight) = self.colors(); let (background, text, shadow, highlight) = self.colors();
buf.set_style(area, Style::new().bg(background).fg(text)); buf.set_style(area, Style::new().bg(background).fg(text));
@@ -144,7 +109,7 @@ impl<'a> Widget for Button<'a> {
} }
impl Button<'_> { impl Button<'_> {
const fn colors(&self) -> (Color, Color, Color, Color) { fn colors(&self) -> (Color, Color, Color, Color) {
let theme = self.theme; let theme = self.theme;
match self.state { match self.state {
State::Normal => (theme.background, theme.text, theme.shadow, theme.highlight), State::Normal => (theme.background, theme.text, theme.shadow, theme.highlight),
@@ -154,11 +119,38 @@ impl Button<'_> {
} }
} }
fn run(mut terminal: DefaultTerminal) -> Result<()> { fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let res = run_app(&mut terminal);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
let mut selected_button: usize = 0; let mut selected_button: usize = 0;
let mut button_states = [State::Selected, State::Normal, State::Normal]; let button_states = &mut [State::Selected, State::Normal, State::Normal];
loop { loop {
terminal.draw(|frame| draw(frame, button_states))?; terminal.draw(|frame| ui(frame, button_states))?;
if !event::poll(Duration::from_millis(100))? { if !event::poll(Duration::from_millis(100))? {
continue; continue;
} }
@@ -167,27 +159,25 @@ fn run(mut terminal: DefaultTerminal) -> Result<()> {
if key.kind != event::KeyEventKind::Press { if key.kind != event::KeyEventKind::Press {
continue; continue;
} }
if handle_key_event(key, &mut button_states, &mut selected_button).is_break() { if handle_key_event(key, button_states, &mut selected_button).is_break() {
break; break;
} }
} }
Event::Mouse(mouse) => { Event::Mouse(mouse) => handle_mouse_event(mouse, button_states, &mut selected_button),
handle_mouse_event(mouse, &mut button_states, &mut selected_button);
}
_ => (), _ => (),
} }
} }
Ok(()) Ok(())
} }
fn draw(frame: &mut Frame, states: [State; 3]) { fn ui(frame: &mut Frame, states: &[State; 3]) {
let vertical = Layout::vertical([ let vertical = Layout::vertical([
Constraint::Length(1), Constraint::Length(1),
Constraint::Max(3), Constraint::Max(3),
Constraint::Length(1), Constraint::Length(1),
Constraint::Min(0), // ignore remaining space Constraint::Min(0), // ignore remaining space
]); ]);
let [title, buttons, help, _] = vertical.areas(frame.area()); let [title, buttons, help, _] = frame.size().split(&vertical);
frame.render_widget( frame.render_widget(
Paragraph::new("Custom Widget Example (mouse enabled)"), Paragraph::new("Custom Widget Example (mouse enabled)"),
@@ -197,14 +187,14 @@ fn draw(frame: &mut Frame, states: [State; 3]) {
frame.render_widget(Paragraph::new("←/→: select, Space: toggle, q: quit"), help); frame.render_widget(Paragraph::new("←/→: select, Space: toggle, q: quit"), help);
} }
fn render_buttons(frame: &mut Frame<'_>, area: Rect, states: [State; 3]) { fn render_buttons(frame: &mut Frame<'_>, area: Rect, states: &[State; 3]) {
let horizontal = Layout::horizontal([ let horizontal = Layout::horizontal([
Constraint::Length(15), Constraint::Length(15),
Constraint::Length(15), Constraint::Length(15),
Constraint::Length(15), Constraint::Length(15),
Constraint::Min(0), // ignore remaining space Constraint::Min(0), // ignore remaining space
]); ]);
let [red, green, blue, _] = horizontal.areas(area); let [red, green, blue, _] = area.split(&horizontal);
frame.render_widget(Button::new("Red").theme(RED).state(states[0]), red); frame.render_widget(Button::new("Red").theme(RED).state(states[0]), red);
frame.render_widget(Button::new("Green").theme(GREEN).state(states[1]), green); frame.render_widget(Button::new("Green").theme(GREEN).state(states[1]), green);

View File

@@ -18,4 +18,4 @@ Space
Left Left
Space Space
Left Left
Space Space

View File

@@ -2,7 +2,7 @@ use rand::{
distributions::{Distribution, Uniform}, distributions::{Distribution, Uniform},
rngs::ThreadRng, rngs::ThreadRng,
}; };
use ratatui::widgets::ListState; use ratatui::widgets::*;
const TASKS: [&str; 24] = [ const TASKS: [&str; 24] = [
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10", "Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10",
@@ -73,8 +73,8 @@ pub struct RandomSignal {
} }
impl RandomSignal { impl RandomSignal {
pub fn new(lower: u64, upper: u64) -> Self { pub fn new(lower: u64, upper: u64) -> RandomSignal {
Self { RandomSignal {
distribution: Uniform::new(lower, upper), distribution: Uniform::new(lower, upper),
rng: rand::thread_rng(), rng: rand::thread_rng(),
} }
@@ -97,8 +97,8 @@ pub struct SinSignal {
} }
impl SinSignal { impl SinSignal {
pub const fn new(interval: f64, period: f64, scale: f64) -> Self { pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
Self { SinSignal {
x: 0.0, x: 0.0,
interval, interval,
period, period,
@@ -122,8 +122,8 @@ pub struct TabsState<'a> {
} }
impl<'a> TabsState<'a> { impl<'a> TabsState<'a> {
pub const fn new(titles: Vec<&'a str>) -> Self { pub fn new(titles: Vec<&'a str>) -> TabsState {
Self { titles, index: 0 } TabsState { titles, index: 0 }
} }
pub fn next(&mut self) { pub fn next(&mut self) {
self.index = (self.index + 1) % self.titles.len(); self.index = (self.index + 1) % self.titles.len();
@@ -144,8 +144,8 @@ pub struct StatefulList<T> {
} }
impl<T> StatefulList<T> { impl<T> StatefulList<T> {
pub fn with_items(items: Vec<T>) -> Self { pub fn with_items(items: Vec<T>) -> StatefulList<T> {
Self { StatefulList {
state: ListState::default(), state: ListState::default(),
items, items,
} }
@@ -191,7 +191,9 @@ where
S: Iterator, S: Iterator,
{ {
fn on_tick(&mut self) { fn on_tick(&mut self) {
self.points.drain(0..self.tick_rate); for _ in 0..self.tick_rate {
self.points.remove(0);
}
self.points self.points
.extend(self.source.by_ref().take(self.tick_rate)); .extend(self.source.by_ref().take(self.tick_rate));
} }
@@ -235,7 +237,7 @@ pub struct App<'a> {
} }
impl<'a> App<'a> { impl<'a> App<'a> {
pub fn new(title: &'a str, enhanced_graphics: bool) -> Self { pub fn new(title: &'a str, enhanced_graphics: bool) -> App<'a> {
let mut rand_signal = RandomSignal::new(0, 100); let mut rand_signal = RandomSignal::new(0, 100);
let sparkline_points = rand_signal.by_ref().take(300).collect(); let sparkline_points = rand_signal.by_ref().take(300).collect();
let mut sin_signal = SinSignal::new(0.2, 3.0, 18.0); let mut sin_signal = SinSignal::new(0.2, 3.0, 18.0);

View File

@@ -4,15 +4,12 @@ use std::{
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use ratatui::{ use crossterm::{
backend::{Backend, CrosstermBackend}, event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
crossterm::{ execute,
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
Terminal,
}; };
use ratatui::prelude::*;
use crate::{app::App, ui}; use crate::{app::App, ui};
@@ -26,7 +23,7 @@ pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn E
// create app and run it // create app and run it
let app = App::new("Crossterm Demo", enhanced_graphics); let app = App::new("Crossterm Demo", enhanced_graphics);
let app_result = run_app(&mut terminal, app, tick_rate); let res = run_app(&mut terminal, app, tick_rate);
// restore terminal // restore terminal
disable_raw_mode()?; disable_raw_mode()?;
@@ -37,7 +34,7 @@ pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn E
)?; )?;
terminal.show_cursor()?; terminal.show_cursor()?;
if let Err(err) = app_result { if let Err(err) = res {
println!("{err:?}"); println!("{err:?}");
} }
@@ -51,10 +48,10 @@ fn run_app<B: Backend>(
) -> io::Result<()> { ) -> io::Result<()> {
let mut last_tick = Instant::now(); let mut last_tick = Instant::now();
loop { loop {
terminal.draw(|frame| ui::draw(frame, &mut app))?; terminal.draw(|f| ui::draw(f, &mut app))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed()); let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout)? { if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press { if key.kind == KeyEventKind::Press {
match key.code { match key.code {

View File

@@ -1,18 +1,3 @@
//! # [Ratatui] Original Demo example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use std::{error::Error, time::Duration}; use std::{error::Error, time::Duration};
use argh::FromArgs; use argh::FromArgs;
@@ -20,7 +5,7 @@ use argh::FromArgs;
mod app; mod app;
#[cfg(feature = "crossterm")] #[cfg(feature = "crossterm")]
mod crossterm; mod crossterm;
#[cfg(all(not(windows), feature = "termion"))] #[cfg(feature = "termion")]
mod termion; mod termion;
#[cfg(feature = "termwiz")] #[cfg(feature = "termwiz")]
mod termwiz; mod termwiz;
@@ -43,13 +28,9 @@ fn main() -> Result<(), Box<dyn Error>> {
let tick_rate = Duration::from_millis(cli.tick_rate); let tick_rate = Duration::from_millis(cli.tick_rate);
#[cfg(feature = "crossterm")] #[cfg(feature = "crossterm")]
crate::crossterm::run(tick_rate, cli.enhanced_graphics)?; crate::crossterm::run(tick_rate, cli.enhanced_graphics)?;
#[cfg(all(not(windows), feature = "termion", not(feature = "crossterm")))] #[cfg(feature = "termion")]
crate::termion::run(tick_rate, cli.enhanced_graphics)?; crate::termion::run(tick_rate, cli.enhanced_graphics)?;
#[cfg(all( #[cfg(feature = "termwiz")]
feature = "termwiz",
not(feature = "crossterm"),
not(feature = "termion")
))]
crate::termwiz::run(tick_rate, cli.enhanced_graphics)?; crate::termwiz::run(tick_rate, cli.enhanced_graphics)?;
Ok(()) Ok(())
} }

View File

@@ -1,15 +1,11 @@
#![allow(dead_code)]
use std::{error::Error, io, sync::mpsc, thread, time::Duration}; use std::{error::Error, io, sync::mpsc, thread, time::Duration};
use ratatui::{ use ratatui::prelude::*;
backend::{Backend, TermionBackend}, use termion::{
termion::{ event::Key,
event::Key, input::{MouseTerminal, TermRead},
input::{MouseTerminal, TermRead}, raw::IntoRawMode,
raw::IntoRawMode, screen::IntoAlternateScreen,
screen::IntoAlternateScreen,
},
Terminal,
}; };
use crate::{app::App, ui}; use crate::{app::App, ui};
@@ -39,7 +35,7 @@ fn run_app<B: Backend>(
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
let events = events(tick_rate); let events = events(tick_rate);
loop { loop {
terminal.draw(|frame| ui::draw(frame, &mut app))?; terminal.draw(|f| ui::draw(f, &mut app))?;
match events.recv()? { match events.recv()? {
Event::Input(key) => match key { Event::Input(key) => match key {

View File

@@ -1,17 +1,10 @@
#![allow(dead_code)]
use std::{ use std::{
error::Error, error::Error,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use ratatui::{ use ratatui::prelude::*;
backend::TermwizBackend, use termwiz::{input::*, terminal::Terminal as TermwizTerminal};
termwiz::{
input::{InputEvent, KeyCode},
terminal::Terminal as TermwizTerminal,
},
Terminal,
};
use crate::{app::App, ui}; use crate::{app::App, ui};
@@ -22,12 +15,12 @@ pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn E
// create app and run it // create app and run it
let app = App::new("Termwiz Demo", enhanced_graphics); let app = App::new("Termwiz Demo", enhanced_graphics);
let app_result = run_app(&mut terminal, app, tick_rate); let res = run_app(&mut terminal, app, tick_rate);
terminal.show_cursor()?; terminal.show_cursor()?;
terminal.flush()?; terminal.flush()?;
if let Err(err) = app_result { if let Err(err) = res {
println!("{err:?}"); println!("{err:?}");
} }
@@ -41,7 +34,7 @@ fn run_app(
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
let mut last_tick = Instant::now(); let mut last_tick = Instant::now();
loop { loop {
terminal.draw(|frame| ui::draw(frame, &mut app))?; terminal.draw(|f| ui::draw(f, &mut app))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed()); let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if let Some(input) = terminal if let Some(input) = terminal

View File

@@ -1,64 +1,56 @@
use ratatui::{ use ratatui::{
layout::{Constraint, Layout, Rect}, prelude::*,
style::{Color, Modifier, Style}, widgets::{canvas::*, *},
symbols,
text::{self, Span},
widgets::{
canvas::{self, Canvas, Circle, Map, MapResolution, Rectangle},
Axis, BarChart, Block, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem, Paragraph,
Row, Sparkline, Table, Tabs, Wrap,
},
Frame,
}; };
use crate::app::App; use crate::app::App;
pub fn draw(frame: &mut Frame, app: &mut App) { pub fn draw(f: &mut Frame, app: &mut App) {
let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(frame.area()); let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(f.size());
let tabs = app let tabs = app
.tabs .tabs
.titles .titles
.iter() .iter()
.map(|t| text::Line::from(Span::styled(*t, Style::default().fg(Color::Green)))) .map(|t| text::Line::from(Span::styled(*t, Style::default().fg(Color::Green))))
.collect::<Tabs>() .collect::<Tabs>()
.block(Block::bordered().title(app.title)) .block(Block::default().borders(Borders::ALL).title(app.title))
.highlight_style(Style::default().fg(Color::Yellow)) .highlight_style(Style::default().fg(Color::Yellow))
.select(app.tabs.index); .select(app.tabs.index);
frame.render_widget(tabs, chunks[0]); f.render_widget(tabs, chunks[0]);
match app.tabs.index { match app.tabs.index {
0 => draw_first_tab(frame, app, chunks[1]), 0 => draw_first_tab(f, app, chunks[1]),
1 => draw_second_tab(frame, app, chunks[1]), 1 => draw_second_tab(f, app, chunks[1]),
2 => draw_third_tab(frame, app, chunks[1]), 2 => draw_third_tab(f, app, chunks[1]),
_ => {} _ => {}
}; };
} }
fn draw_first_tab(frame: &mut Frame, app: &mut App, area: Rect) { fn draw_first_tab(f: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::vertical([ let chunks = Layout::vertical([
Constraint::Length(9), Constraint::Length(9),
Constraint::Min(8), Constraint::Min(8),
Constraint::Length(7), Constraint::Length(7),
]) ])
.split(area); .split(area);
draw_gauges(frame, app, chunks[0]); draw_gauges(f, app, chunks[0]);
draw_charts(frame, app, chunks[1]); draw_charts(f, app, chunks[1]);
draw_text(frame, chunks[2]); draw_text(f, chunks[2]);
} }
fn draw_gauges(frame: &mut Frame, app: &mut App, area: Rect) { fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
let chunks = Layout::vertical([ let chunks = Layout::vertical([
Constraint::Length(2), Constraint::Length(2),
Constraint::Length(3), Constraint::Length(3),
Constraint::Length(2), Constraint::Length(1),
]) ])
.margin(1) .margin(1)
.split(area); .split(area);
let block = Block::bordered().title("Graphs"); let block = Block::default().borders(Borders::ALL).title("Graphs");
frame.render_widget(block, area); f.render_widget(block, area);
let label = format!("{:.2}%", app.progress * 100.0); let label = format!("{:.2}%", app.progress * 100.0);
let gauge = Gauge::default() let gauge = Gauge::default()
.block(Block::new().title("Gauge:")) .block(Block::default().title("Gauge:"))
.gauge_style( .gauge_style(
Style::default() Style::default()
.fg(Color::Magenta) .fg(Color::Magenta)
@@ -68,10 +60,10 @@ fn draw_gauges(frame: &mut Frame, app: &mut App, area: Rect) {
.use_unicode(app.enhanced_graphics) .use_unicode(app.enhanced_graphics)
.label(label) .label(label)
.ratio(app.progress); .ratio(app.progress);
frame.render_widget(gauge, chunks[0]); f.render_widget(gauge, chunks[0]);
let sparkline = Sparkline::default() let sparkline = Sparkline::default()
.block(Block::new().title("Sparkline:")) .block(Block::default().title("Sparkline:"))
.style(Style::default().fg(Color::Green)) .style(Style::default().fg(Color::Green))
.data(&app.sparkline.points) .data(&app.sparkline.points)
.bar_set(if app.enhanced_graphics { .bar_set(if app.enhanced_graphics {
@@ -79,22 +71,21 @@ fn draw_gauges(frame: &mut Frame, app: &mut App, area: Rect) {
} else { } else {
symbols::bar::THREE_LEVELS symbols::bar::THREE_LEVELS
}); });
frame.render_widget(sparkline, chunks[1]); f.render_widget(sparkline, chunks[1]);
let line_gauge = LineGauge::default() let line_gauge = LineGauge::default()
.block(Block::new().title("LineGauge:")) .block(Block::default().title("LineGauge:"))
.filled_style(Style::default().fg(Color::Magenta)) .gauge_style(Style::default().fg(Color::Magenta))
.line_set(if app.enhanced_graphics { .line_set(if app.enhanced_graphics {
symbols::line::THICK symbols::line::THICK
} else { } else {
symbols::line::NORMAL symbols::line::NORMAL
}) })
.ratio(app.progress); .ratio(app.progress);
frame.render_widget(line_gauge, chunks[2]); f.render_widget(line_gauge, chunks[2]);
} }
#[allow(clippy::too_many_lines)] fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) {
let constraints = if app.show_chart { let constraints = if app.show_chart {
vec![Constraint::Percentage(50), Constraint::Percentage(50)] vec![Constraint::Percentage(50), Constraint::Percentage(50)]
} else { } else {
@@ -117,10 +108,10 @@ fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) {
.map(|i| ListItem::new(vec![text::Line::from(Span::raw(*i))])) .map(|i| ListItem::new(vec![text::Line::from(Span::raw(*i))]))
.collect(); .collect();
let tasks = List::new(tasks) let tasks = List::new(tasks)
.block(Block::bordered().title("List")) .block(Block::default().borders(Borders::ALL).title("List"))
.highlight_style(Style::default().add_modifier(Modifier::BOLD)) .highlight_style(Style::default().add_modifier(Modifier::BOLD))
.highlight_symbol("> "); .highlight_symbol("> ");
frame.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state); f.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state);
// Draw logs // Draw logs
let info_style = Style::default().fg(Color::Blue); let info_style = Style::default().fg(Color::Blue);
@@ -145,12 +136,12 @@ fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) {
ListItem::new(content) ListItem::new(content)
}) })
.collect(); .collect();
let logs = List::new(logs).block(Block::bordered().title("List")); let logs = List::new(logs).block(Block::default().borders(Borders::ALL).title("List"));
frame.render_stateful_widget(logs, chunks[1], &mut app.logs.state); f.render_stateful_widget(logs, chunks[1], &mut app.logs.state);
} }
let barchart = BarChart::default() let barchart = BarChart::default()
.block(Block::bordered().title("Bar chart")) .block(Block::default().borders(Borders::ALL).title("Bar chart"))
.data(&app.barchart) .data(&app.barchart)
.bar_width(3) .bar_width(3)
.bar_gap(2) .bar_gap(2)
@@ -167,7 +158,7 @@ fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) {
) )
.label_style(Style::default().fg(Color::Yellow)) .label_style(Style::default().fg(Color::Yellow))
.bar_style(Style::default().fg(Color::Green)); .bar_style(Style::default().fg(Color::Green));
frame.render_widget(barchart, chunks[1]); f.render_widget(barchart, chunks[1]);
} }
if app.show_chart { if app.show_chart {
let x_labels = vec![ let x_labels = vec![
@@ -202,12 +193,14 @@ fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) {
]; ];
let chart = Chart::new(datasets) let chart = Chart::new(datasets)
.block( .block(
Block::bordered().title(Span::styled( Block::default()
"Chart", .title(Span::styled(
Style::default() "Chart",
.fg(Color::Cyan) Style::default()
.add_modifier(Modifier::BOLD), .fg(Color::Cyan)
)), .add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL),
) )
.x_axis( .x_axis(
Axis::default() Axis::default()
@@ -221,17 +214,17 @@ fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) {
.title("Y Axis") .title("Y Axis")
.style(Style::default().fg(Color::Gray)) .style(Style::default().fg(Color::Gray))
.bounds([-20.0, 20.0]) .bounds([-20.0, 20.0])
.labels([ .labels(vec![
Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)), Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)),
Span::raw("0"), Span::raw("0"),
Span::styled("20", Style::default().add_modifier(Modifier::BOLD)), Span::styled("20", Style::default().add_modifier(Modifier::BOLD)),
]), ]),
); );
frame.render_widget(chart, chunks[1]); f.render_widget(chart, chunks[1]);
} }
} }
fn draw_text(frame: &mut Frame, area: Rect) { fn draw_text(f: &mut Frame, area: Rect) {
let text = vec![ let text = vec![
text::Line::from("This is a paragraph with several lines. You can change style your text the way you want"), text::Line::from("This is a paragraph with several lines. You can change style your text the way you want"),
text::Line::from(""), text::Line::from(""),
@@ -259,17 +252,17 @@ fn draw_text(frame: &mut Frame, area: Rect) {
"One more thing is that it should display unicode characters: 10€" "One more thing is that it should display unicode characters: 10€"
), ),
]; ];
let block = Block::bordered().title(Span::styled( let block = Block::default().borders(Borders::ALL).title(Span::styled(
"Footer", "Footer",
Style::default() Style::default()
.fg(Color::Magenta) .fg(Color::Magenta)
.add_modifier(Modifier::BOLD), .add_modifier(Modifier::BOLD),
)); ));
let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true }); let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
frame.render_widget(paragraph, area); f.render_widget(paragraph, area);
} }
fn draw_second_tab(frame: &mut Frame, app: &mut App, area: Rect) { fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) {
let chunks = let chunks =
Layout::horizontal([Constraint::Percentage(30), Constraint::Percentage(70)]).split(area); Layout::horizontal([Constraint::Percentage(30), Constraint::Percentage(70)]).split(area);
let up_style = Style::default().fg(Color::Green); let up_style = Style::default().fg(Color::Green);
@@ -297,11 +290,11 @@ fn draw_second_tab(frame: &mut Frame, app: &mut App, area: Rect) {
.style(Style::default().fg(Color::Yellow)) .style(Style::default().fg(Color::Yellow))
.bottom_margin(1), .bottom_margin(1),
) )
.block(Block::bordered().title("Servers")); .block(Block::default().title("Servers").borders(Borders::ALL));
frame.render_widget(table, chunks[0]); f.render_widget(table, chunks[0]);
let map = Canvas::default() let map = Canvas::default()
.block(Block::bordered().title("World")) .block(Block::default().title("World").borders(Borders::ALL))
.paint(|ctx| { .paint(|ctx| {
ctx.draw(&Map { ctx.draw(&Map {
color: Color::White, color: Color::White,
@@ -352,10 +345,10 @@ fn draw_second_tab(frame: &mut Frame, app: &mut App, area: Rect) {
}) })
.x_bounds([-180.0, 180.0]) .x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0]); .y_bounds([-90.0, 90.0]);
frame.render_widget(map, chunks[1]); f.render_widget(map, chunks[1]);
} }
fn draw_third_tab(frame: &mut Frame, _app: &mut App, area: Rect) { fn draw_third_tab(f: &mut Frame, _app: &mut App, area: Rect) {
let chunks = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).split(area); let chunks = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).split(area);
let colors = [ let colors = [
Color::Reset, Color::Reset,
@@ -395,6 +388,6 @@ fn draw_third_tab(frame: &mut Frame, _app: &mut App, area: Rect) {
Constraint::Ratio(1, 3), Constraint::Ratio(1, 3),
], ],
) )
.block(Block::bordered().title("Colors")); .block(Block::default().title("Colors").borders(Borders::ALL));
frame.render_widget(table, chunks[0]); f.render_widget(table, chunks[0]);
} }

View File

@@ -1,5 +1,5 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info. # This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/demo2-destroy.tape` # To run this script, install vhs and run `vhs ./examples/demo.tape`
# NOTE: Requires VHS 0.6.1 or later for Screenshot support # NOTE: Requires VHS 0.6.1 or later for Screenshot support
Output "target/demo2-destroy.gif" Output "target/demo2-destroy.gif"
Set Theme "Aardvark Blue" Set Theme "Aardvark Blue"
@@ -10,9 +10,9 @@ Set Width 1120
Set Height 480 Set Height 480
Set Padding 0 Set Padding 0
Hide Hide
Type "cargo run --example demo2 --features crossterm,palette,widget-calendar" Type "cargo run --example demo2 --features crossterm,widget-calendar"
Enter Enter
Sleep 2s Sleep 2s
Show Show
Type "d" Type "d"
Sleep 20s Sleep 30s

View File

@@ -1,5 +1,5 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info. # This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/demo2-social.tape` # To run this script, install vhs and run `vhs ./examples/demo.tape`
Output "target/demo2-social.gif" Output "target/demo2-social.gif"
Set Theme "Aardvark Blue" Set Theme "Aardvark Blue"
@@ -40,4 +40,4 @@ Tab
# Weather # Weather
Set TypingSpeed 100ms Set TypingSpeed 100ms
Down 40 Down 40
Sleep 2s Sleep 2s

View File

@@ -1,5 +1,5 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info. # This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/demo2.tape` # To run this script, install vhs and run `vhs ./examples/demo.tape`
# NOTE: Requires VHS 0.6.1 or later for Screenshot support # NOTE: Requires VHS 0.6.1 or later for Screenshot support
Output "target/demo2.gif" Output "target/demo2.gif"
Set Theme "Aardvark Blue" Set Theme "Aardvark Blue"
@@ -46,4 +46,4 @@ Tab
Screenshot "target/demo2-weather.png" Screenshot "target/demo2-weather.png"
Set TypingSpeed 100ms Set TypingSpeed 100ms
Down 40 Down 40
Sleep 2s Sleep 2s

View File

@@ -1,225 +1,252 @@
use std::time::Duration; use std::time::Duration;
use color_eyre::{eyre::Context, Result}; use anyhow::{Context, Result};
use crossterm::event; use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use itertools::Itertools; use rand::Rng;
use ratatui::{ use rand_chacha::rand_core::SeedableRng;
buffer::Buffer, use ratatui::{buffer::Cell, layout::Flex, prelude::*, widgets::Widget};
crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind}, use unicode_width::UnicodeWidthStr;
layout::{Constraint, Layout, Rect},
style::Color,
text::{Line, Span},
widgets::{Block, Tabs, Widget},
DefaultTerminal, Frame,
};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
use crate::{ use crate::{
destroy, big_text::{BigTextBuilder, PixelSize},
tabs::{AboutTab, EmailTab, RecipeTab, TracerouteTab, WeatherTab}, Root, Term,
THEME,
}; };
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] #[derive(Debug)]
pub struct App { pub struct App {
term: Term,
context: AppContext,
mode: Mode, mode: Mode,
tab: Tab,
about_tab: AboutTab,
recipe_tab: RecipeTab,
email_tab: EmailTab,
traceroute_tab: TracerouteTab,
weather_tab: WeatherTab,
} }
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum Mode { enum Mode {
#[default] #[default]
Running, Normal,
Destroy, Destroy,
Quit, Quit,
} }
#[derive(Debug, Clone, Copy, Default, Display, EnumIter, FromRepr, PartialEq, Eq)] #[derive(Debug, Default, Clone, Copy)]
enum Tab { pub struct AppContext {
#[default] pub tab_index: usize,
About, pub row_index: usize,
Recipe,
Email,
Traceroute,
Weather,
} }
impl App { impl App {
/// Run the app until the user quits. fn new() -> Result<Self> {
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { Ok(Self {
while self.is_running() { term: Term::start()?,
terminal context: AppContext::default(),
.draw(|frame| self.draw(frame)) mode: Mode::Normal,
.wrap_err("terminal.draw")?; })
self.handle_events()?; }
pub fn run() -> Result<()> {
install_panic_hook();
let mut app = Self::new()?;
while !app.should_quit() {
app.draw()?;
app.handle_events()?;
} }
Term::stop()?;
Ok(()) Ok(())
} }
fn is_running(&self) -> bool { fn draw(&mut self) -> Result<()> {
self.mode != Mode::Quit self.term
.draw(|frame| {
frame.render_widget(Root::new(&self.context), frame.size());
if self.mode == Mode::Destroy {
destroy(frame);
}
})
.context("terminal.draw")?;
Ok(())
} }
/// Draw a single frame of the app. fn handle_events(&mut self) -> Result<()> {
fn draw(&self, frame: &mut Frame) { // https://superuser.com/questions/1449366/do-60-fps-gifs-actually-exist-or-is-the-maximum-50-fps
frame.render_widget(self, frame.area()); const GIF_FRAME_RATE: f64 = 50.0;
if self.mode == Mode::Destroy { match Term::next_event(Duration::from_secs_f64(1.0 / GIF_FRAME_RATE))? {
destroy::destroy(frame); Some(Event::Key(key)) => self.handle_key_event(key),
Some(Event::Resize(width, height)) => {
Ok(self.term.resize(Rect::new(0, 0, width, height))?)
}
_ => Ok(()),
} }
} }
/// Handle events from the terminal. fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> {
/// if key.kind != KeyEventKind::Press {
/// This function is called once per frame, The events are polled from the stdin with timeout of
/// 1/50th of a second. This was chosen to try to match the default frame rate of a GIF in VHS.
fn handle_events(&mut self) -> Result<()> {
let timeout = Duration::from_secs_f64(1.0 / 50.0);
if !event::poll(timeout)? {
return Ok(()); return Ok(());
} }
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => self.handle_key_press(key), let context = &mut self.context;
const TAB_COUNT: usize = 5;
match key.code {
KeyCode::Char('q') | KeyCode::Esc => {
self.mode = Mode::Quit;
}
KeyCode::Tab | KeyCode::BackTab if key.modifiers.contains(KeyModifiers::SHIFT) => {
let tab_index = context.tab_index + TAB_COUNT; // to wrap around properly
context.tab_index = tab_index.saturating_sub(1) % TAB_COUNT;
context.row_index = 0;
}
KeyCode::Tab | KeyCode::BackTab => {
context.tab_index = context.tab_index.saturating_add(1) % TAB_COUNT;
context.row_index = 0;
}
KeyCode::Up | KeyCode::Char('k') => {
context.row_index = context.row_index.saturating_sub(1);
}
KeyCode::Down | KeyCode::Char('j') => {
context.row_index = context.row_index.saturating_add(1);
}
KeyCode::Char('d') => {
self.mode = Mode::Destroy;
}
_ => {} _ => {}
} };
Ok(()) Ok(())
} }
fn handle_key_press(&mut self, key: KeyEvent) { fn should_quit(&self) -> bool {
match key.code { self.mode == Mode::Quit
KeyCode::Char('q') | KeyCode::Esc => self.mode = Mode::Quit,
KeyCode::Char('h') | KeyCode::Left => self.prev_tab(),
KeyCode::Char('l') | KeyCode::Right => self.next_tab(),
KeyCode::Char('k') | KeyCode::Up => self.prev(),
KeyCode::Char('j') | KeyCode::Down => self.next(),
KeyCode::Char('d') | KeyCode::Delete => self.destroy(),
_ => {}
};
}
fn prev(&mut self) {
match self.tab {
Tab::About => self.about_tab.prev_row(),
Tab::Recipe => self.recipe_tab.prev(),
Tab::Email => self.email_tab.prev(),
Tab::Traceroute => self.traceroute_tab.prev_row(),
Tab::Weather => self.weather_tab.prev(),
}
}
fn next(&mut self) {
match self.tab {
Tab::About => self.about_tab.next_row(),
Tab::Recipe => self.recipe_tab.next(),
Tab::Email => self.email_tab.next(),
Tab::Traceroute => self.traceroute_tab.next_row(),
Tab::Weather => self.weather_tab.next(),
}
}
fn prev_tab(&mut self) {
self.tab = self.tab.prev();
}
fn next_tab(&mut self) {
self.tab = self.tab.next();
}
fn destroy(&mut self) {
self.mode = Mode::Destroy;
} }
} }
/// Implement Widget for &App rather than for App as we would otherwise have to clone or copy the /// delay the start of the animation so it doesn't start immediately
/// entire app state on every frame. For this example, the app state is small enough that it doesn't const DELAY: usize = 240;
/// matter, but for larger apps this can be a significant performance improvement. /// higher means more pixels per frame are modified in the animation
impl Widget for &App { const DRIP_SPEED: usize = 50;
fn render(self, area: Rect, buf: &mut Buffer) { /// delay the start of the text animation so it doesn't start immediately after the initial delay
let vertical = Layout::vertical([ const TEXT_DELAY: usize = 240;
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
]);
let [title_bar, tab, bottom_bar] = vertical.areas(area);
Block::new().style(THEME.root).render(area, buf); /// Destroy mode activated by pressing `d`
self.render_title_bar(title_bar, buf); fn destroy(frame: &mut Frame<'_>) {
self.render_selected_tab(tab, buf); let frame_count = frame.count().saturating_sub(DELAY);
App::render_bottom_bar(bottom_bar, buf); if frame_count == 0 {
return;
} }
let area = frame.size();
let buf = frame.buffer_mut();
drip(frame_count, area, buf);
text(frame_count, area, buf);
} }
impl App { /// Move a bunch of random pixels down one row.
fn render_title_bar(&self, area: Rect, buf: &mut Buffer) { ///
let layout = Layout::horizontal([Constraint::Min(0), Constraint::Length(43)]); /// Each pick some random pixels and move them each down one row. This is a very inefficient way to
let [title, tabs] = layout.areas(area); /// do this, but it works well enough for this demo.
fn drip(frame_count: usize, area: Rect, buf: &mut Buffer) {
// a seeded rng as we have to move the same random pixels each frame
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(10);
let ramp_frames = 450;
let fractional_speed = frame_count as f64 / ramp_frames as f64;
let variable_speed = DRIP_SPEED as f64 * fractional_speed * fractional_speed * fractional_speed;
let pixel_count = (frame_count as f64 * variable_speed).floor() as usize;
for _ in 0..pixel_count {
let src_x = rng.gen_range(0..area.width);
let src_y = rng.gen_range(1..area.height - 2);
let src = buf.get_mut(src_x, src_y).clone();
// 1% of the time, move a blank or pixel (10:1) to the top line of the screen
if rng.gen_ratio(1, 100) {
let dest_x = rng
.gen_range(src_x.saturating_sub(5)..src_x.saturating_add(5))
.clamp(area.left(), area.right() - 1);
let dest_y = area.top() + 1;
Span::styled("Ratatui", THEME.app_title).render(title, buf); let dest = buf.get_mut(dest_x, dest_y);
let titles = Tab::iter().map(Tab::title); // copy the cell to the new location about 1/10 of the time blank out the cell the rest
Tabs::new(titles) // of the time. This has the effect of gradually removing the pixels from the screen.
.style(THEME.tabs) if rng.gen_ratio(1, 10) {
.highlight_style(THEME.tabs_selected) *dest = src;
.select(self.tab as usize) } else {
.divider("") *dest = Cell::default();
.padding("", "") }
.render(tabs, buf); } else {
} // move the pixel down one row
let dest_x = src_x;
fn render_selected_tab(&self, area: Rect, buf: &mut Buffer) { let dest_y = src_y.saturating_add(1).min(area.bottom() - 2);
match self.tab { // copy the cell to the new location
Tab::About => self.about_tab.render(area, buf), let dest = buf.get_mut(dest_x, dest_y);
Tab::Recipe => self.recipe_tab.render(area, buf), *dest = src;
Tab::Email => self.email_tab.render(area, buf),
Tab::Traceroute => self.traceroute_tab.render(area, buf),
Tab::Weather => self.weather_tab.render(area, buf),
};
}
fn render_bottom_bar(area: Rect, buf: &mut Buffer) {
let keys = [
("H/←", "Left"),
("L/→", "Right"),
("K/↑", "Up"),
("J/↓", "Down"),
("D/Del", "Destroy"),
("Q/Esc", "Quit"),
];
let spans = keys
.iter()
.flat_map(|(key, desc)| {
let key = Span::styled(format!(" {key} "), THEME.key_binding.key);
let desc = Span::styled(format!(" {desc} "), THEME.key_binding.description);
[key, desc]
})
.collect_vec();
Line::from(spans)
.centered()
.style((Color::Indexed(236), Color::Indexed(232)))
.render(area, buf);
}
}
impl Tab {
fn next(self) -> Self {
let current_index = self as usize;
let next_index = current_index.saturating_add(1);
Self::from_repr(next_index).unwrap_or(self)
}
fn prev(self) -> Self {
let current_index = self as usize;
let prev_index = current_index.saturating_sub(1);
Self::from_repr(prev_index).unwrap_or(self)
}
fn title(self) -> String {
match self {
Self::About => String::new(),
tab => format!(" {tab} "),
} }
} }
} }
/// draw some text fading in and out from black to red and back
fn text(frame_count: usize, area: Rect, buf: &mut Buffer) {
let sub_frame = frame_count.saturating_sub(TEXT_DELAY);
if sub_frame == 0 {
return;
}
let line = "RATATUI";
let big_text = BigTextBuilder::default()
.lines([line.into()])
.pixel_size(PixelSize::Full)
.style(Style::new().fg(Color::Rgb(255, 0, 0)))
.build()
.unwrap();
// the font size is 8x8 for each character and we have 1 line
let area = centered_rect(area, line.width() as u16 * 8, 8);
let mask_buf = &mut Buffer::empty(area);
big_text.render(area, mask_buf);
let percentage = (sub_frame as f64 / 480.0).clamp(0.0, 1.0);
for row in area.rows() {
for col in row.columns() {
let cell = buf.get_mut(col.x, col.y);
let mask_cell = mask_buf.get(col.x, col.y);
cell.set_symbol(mask_cell.symbol());
// blend the mask cell color with the cell color
let cell_color = cell.style().bg.unwrap_or(Color::Rgb(0, 0, 0));
let mask_color = mask_cell.style().fg.unwrap_or(Color::Rgb(255, 0, 0));
let color = blend(mask_color, cell_color, percentage);
cell.set_style(Style::new().fg(color));
}
}
}
fn blend(mask_color: Color, cell_color: Color, percentage: f64) -> Color {
let Color::Rgb(mask_red, mask_green, mask_blue) = mask_color else {
return mask_color;
};
let Color::Rgb(cell_red, cell_green, cell_blue) = cell_color else {
return mask_color;
};
let red = mask_red as f64 * percentage + cell_red as f64 * (1.0 - percentage);
let green = mask_green as f64 * percentage + cell_green as f64 * (1.0 - percentage);
let blue = mask_blue as f64 * percentage + cell_blue as f64 * (1.0 - percentage);
Color::Rgb(red as u8, green as u8, blue as u8)
}
/// a centered rect of the given size
fn centered_rect(area: Rect, width: u16, height: u16) -> Rect {
let horizontal = Layout::horizontal([width]).flex(Flex::Center);
let vertical = Layout::vertical([height]).flex(Flex::Center);
let [area] = area.split(&vertical);
let [area] = area.split(&horizontal);
area
}
pub fn install_panic_hook() {
better_panic::install();
let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let _ = Term::stop();
hook(info);
std::process::exit(1);
}));
}

815
examples/demo2/big_text.rs Normal file
View File

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

View File

@@ -1,5 +1,5 @@
use palette::{IntoColor, Okhsv, Srgb}; use palette::{IntoColor, Okhsv, Srgb};
use ratatui::{buffer::Buffer, layout::Rect, style::Color, widgets::Widget}; use ratatui::{prelude::*, widgets::*};
/// A widget that renders a color swatch of RGB colors. /// A widget that renders a color swatch of RGB colors.
/// ///
@@ -9,23 +9,22 @@ use ratatui::{buffer::Buffer, layout::Rect, style::Color, widgets::Widget};
pub struct RgbSwatch; pub struct RgbSwatch;
impl Widget for RgbSwatch { impl Widget for RgbSwatch {
#[allow(clippy::cast_precision_loss, clippy::similar_names)]
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
for (yi, y) in (area.top()..area.bottom()).enumerate() { for (yi, y) in (area.top()..area.bottom()).enumerate() {
let value = f32::from(area.height) - yi as f32; let value = area.height as f32 - yi as f32;
let value_fg = value / f32::from(area.height); let value_fg = value / (area.height as f32);
let value_bg = (value - 0.5) / f32::from(area.height); let value_bg = (value - 0.5) / (area.height as f32);
for (xi, x) in (area.left()..area.right()).enumerate() { for (xi, x) in (area.left()..area.right()).enumerate() {
let hue = xi as f32 * 360.0 / f32::from(area.width); let hue = xi as f32 * 360.0 / area.width as f32;
let fg = color_from_oklab(hue, Okhsv::max_saturation(), value_fg); let fg = color_from_oklab(hue, Okhsv::max_saturation(), value_fg);
let bg = color_from_oklab(hue, Okhsv::max_saturation(), value_bg); let bg = color_from_oklab(hue, Okhsv::max_saturation(), value_bg);
buf[(x, y)].set_char('▀').set_fg(fg).set_bg(bg); buf.get_mut(x, y).set_char('▀').set_fg(fg).set_bg(bg);
} }
} }
} }
} }
/// Convert a hue and value into an RGB color via the Oklab color space. /// Convert a hue and value into an RGB color via the OkLab color space.
/// ///
/// See <https://bottosson.github.io/posts/oklab/> for more details. /// See <https://bottosson.github.io/posts/oklab/> for more details.
pub fn color_from_oklab(hue: f32, saturation: f32, value: f32) -> Color { pub fn color_from_oklab(hue: f32, saturation: f32, value: f32) -> Color {

View File

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

View File

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

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

@@ -0,0 +1,79 @@
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use crate::{tabs::*, AppContext, THEME};
pub struct Root<'a> {
context: &'a AppContext,
}
impl<'a> Root<'a> {
pub fn new(context: &'a AppContext) -> Self {
Root { context }
}
}
impl Widget for Root<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
Block::new().style(THEME.root).render(area, buf);
let vertical = Layout::vertical([
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
]);
let [title_bar, tab, bottom_bar] = area.split(&vertical);
self.render_title_bar(title_bar, buf);
self.render_selected_tab(tab, buf);
self.render_bottom_bar(bottom_bar, buf);
}
}
impl Root<'_> {
fn render_title_bar(&self, area: Rect, buf: &mut Buffer) {
let horizontal = Layout::horizontal([Constraint::Min(0), Constraint::Length(45)]);
let [title, tabs] = area.split(&horizontal);
Paragraph::new(Span::styled("Ratatui", THEME.app_title)).render(title, buf);
let titles = vec!["", " Recipe ", " Email ", " Traceroute ", " Weather "];
Tabs::new(titles)
.style(THEME.tabs)
.highlight_style(THEME.tabs_selected)
.select(self.context.tab_index)
.divider("")
.render(tabs, buf);
}
fn render_selected_tab(&self, area: Rect, buf: &mut Buffer) {
let row_index = self.context.row_index;
match self.context.tab_index {
0 => AboutTab::new(row_index).render(area, buf),
1 => RecipeTab::new(row_index).render(area, buf),
2 => EmailTab::new(row_index).render(area, buf),
3 => TracerouteTab::new(row_index).render(area, buf),
4 => WeatherTab::new(row_index).render(area, buf),
_ => unreachable!(),
};
}
fn render_bottom_bar(&self, area: Rect, buf: &mut Buffer) {
let keys = [
("Q/Esc", "Quit"),
("Tab", "Next Tab"),
("↑/k", "Up"),
("↓/j", "Down"),
];
let spans = keys
.iter()
.flat_map(|(key, desc)| {
let key = Span::styled(format!(" {} ", key), THEME.key_binding.key);
let desc = Span::styled(format!(" {} ", desc), THEME.key_binding.description);
[key, desc]
})
.collect_vec();
Paragraph::new(Line::from(spans))
.alignment(Alignment::Center)
.fg(Color::Indexed(236))
.bg(Color::Indexed(232))
.render(area, buf);
}
}

View File

@@ -1,9 +1,5 @@
use itertools::Itertools; use itertools::Itertools;
use ratatui::{ use ratatui::{prelude::*, widgets::*};
buffer::Buffer,
layout::{Alignment, Constraint, Layout, Margin, Rect},
widgets::{Block, Borders, Clear, Padding, Paragraph, Widget, Wrap},
};
use crate::{RgbSwatch, THEME}; use crate::{RgbSwatch, THEME};
@@ -42,18 +38,13 @@ const RATATUI_LOGO: [&str; 32] = [
" █xxxxxxxxxxxxxxxxxxxxx█ █ ", " █xxxxxxxxxxxxxxxxxxxxx█ █ ",
]; ];
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct AboutTab { pub struct AboutTab {
row_index: usize, selected_row: usize,
} }
impl AboutTab { impl AboutTab {
pub fn prev_row(&mut self) { pub fn new(selected_row: usize) -> Self {
self.row_index = self.row_index.saturating_sub(1); Self { selected_row }
}
pub fn next_row(&mut self) {
self.row_index = self.row_index.saturating_add(1);
} }
} }
@@ -61,23 +52,27 @@ impl Widget for AboutTab {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
RgbSwatch.render(area, buf); RgbSwatch.render(area, buf);
let horizontal = Layout::horizontal([Constraint::Length(34), Constraint::Min(0)]); let horizontal = Layout::horizontal([Constraint::Length(34), Constraint::Min(0)]);
let [description, logo] = horizontal.areas(area); let [description, logo] = area.split(&horizontal);
render_crate_description(description, buf); render_crate_description(description, buf);
render_logo(self.row_index, logo, buf); render_logo(self.selected_row, logo, buf);
} }
} }
fn render_crate_description(area: Rect, buf: &mut Buffer) { fn render_crate_description(area: Rect, buf: &mut Buffer) {
let area = area.inner(Margin { let area = area.inner(
vertical: 4, &(Margin {
horizontal: 2, vertical: 4,
}); horizontal: 2,
}),
);
Clear.render(area, buf); // clear out the color swatches Clear.render(area, buf); // clear out the color swatches
Block::new().style(THEME.content).render(area, buf); Block::new().style(THEME.content).render(area, buf);
let area = area.inner(Margin { let area = area.inner(
vertical: 1, &(Margin {
horizontal: 2, vertical: 1,
}); horizontal: 2,
}),
);
let text = "- cooking up terminal user interfaces - let text = "- cooking up terminal user interfaces -
Ratatui is a Rust crate that provides widgets (e.g. Paragraph, Table) and draws them to the \ Ratatui is a Rust crate that provides widgets (e.g. Paragraph, Table) and draws them to the \
@@ -97,18 +92,17 @@ fn render_crate_description(area: Rect, buf: &mut Buffer) {
.render(area, buf); .render(area, buf);
} }
/// Use half block characters to render a logo based on the `RATATUI_LOGO` const. /// Use half block characters to render a logo based on the RATATUI_LOGO const.
/// ///
/// The logo is rendered in three colors, one for the rat, one for the terminal, and one for the /// The logo is rendered in three colors, one for the rat, one for the terminal, and one for the
/// rat's eye. The eye color alternates between two colors based on the selected row. /// rat's eye. The eye color alternates between two colors based on the selected row.
#[allow(clippy::cast_possible_truncation)]
pub fn render_logo(selected_row: usize, area: Rect, buf: &mut Buffer) { pub fn render_logo(selected_row: usize, area: Rect, buf: &mut Buffer) {
let eye_color = if selected_row % 2 == 0 { let eye_color = if selected_row % 2 == 0 {
THEME.logo.rat_eye THEME.logo.rat_eye
} else { } else {
THEME.logo.rat_eye_alt THEME.logo.rat_eye_alt
}; };
let area = area.inner(Margin { let area = area.inner(&Margin {
vertical: 0, vertical: 0,
horizontal: 2, horizontal: 2,
}); });
@@ -116,7 +110,7 @@ pub fn render_logo(selected_row: usize, area: Rect, buf: &mut Buffer) {
for (x, (ch1, ch2)) in line1.chars().zip(line2.chars()).enumerate() { for (x, (ch1, ch2)) in line1.chars().zip(line2.chars()).enumerate() {
let x = area.left() + x as u16; let x = area.left() + x as u16;
let y = area.top() + y as u16; let y = area.top() + y as u16;
let cell = &mut buf[(x, y)]; let cell = buf.get_mut(x, y);
let rat_color = THEME.logo.rat; let rat_color = THEME.logo.rat;
let term_color = THEME.logo.term; let term_color = THEME.logo.term;
match (ch1, ch2) { match (ch1, ch2) {

View File

@@ -1,14 +1,5 @@
use itertools::Itertools; use itertools::Itertools;
use ratatui::{ use ratatui::{prelude::*, widgets::*};
buffer::Buffer,
layout::{Constraint, Layout, Margin, Rect},
style::{Styled, Stylize},
text::Line,
widgets::{
Block, BorderType, Borders, Clear, List, ListItem, ListState, Padding, Paragraph,
Scrollbar, ScrollbarState, StatefulWidget, Tabs, Widget,
},
};
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use crate::{RgbSwatch, THEME}; use crate::{RgbSwatch, THEME};
@@ -48,40 +39,36 @@ const EMAILS: &[Email] = &[
}, },
]; ];
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] #[derive(Debug, Default)]
pub struct EmailTab { pub struct EmailTab {
row_index: usize, selected_index: usize,
} }
impl EmailTab { impl EmailTab {
/// Select the previous email (with wrap around). pub fn new(selected_index: usize) -> Self {
pub fn prev(&mut self) { Self {
self.row_index = self.row_index.saturating_add(EMAILS.len() - 1) % EMAILS.len(); selected_index: selected_index % EMAILS.len(),
} }
/// Select the next email (with wrap around).
pub fn next(&mut self) {
self.row_index = self.row_index.saturating_add(1) % EMAILS.len();
} }
} }
impl Widget for EmailTab { impl Widget for EmailTab {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
RgbSwatch.render(area, buf); RgbSwatch.render(area, buf);
let area = area.inner(Margin { let area = area.inner(&Margin {
vertical: 1, vertical: 1,
horizontal: 2, horizontal: 2,
}); });
Clear.render(area, buf); Clear.render(area, buf);
let vertical = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]); let vertical = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]);
let [inbox, email] = vertical.areas(area); let [inbox, email] = area.split(&vertical);
render_inbox(self.row_index, inbox, buf); render_inbox(self.selected_index, inbox, buf);
render_email(self.row_index, email, buf); render_email(self.selected_index, email, buf);
} }
} }
fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) { fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]); let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
let [tabs, inbox] = vertical.areas(area); let [tabs, inbox] = area.split(&vertical);
let theme = THEME.email; let theme = THEME.email;
Tabs::new(vec![" Inbox ", " Sent ", " Drafts "]) Tabs::new(vec![" Inbox ", " Sent ", " Drafts "])
.style(theme.tabs) .style(theme.tabs)
@@ -96,10 +83,13 @@ fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
.map(|e| e.from.width()) .map(|e| e.from.width())
.max() .max()
.unwrap_or_default(); .unwrap_or_default();
let items = EMAILS.iter().map(|e| { let items = EMAILS
let from = format!("{:width$}", e.from, width = from_width).into(); .iter()
ListItem::new(Line::from(vec![from, " ".into(), e.subject.into()])) .map(|e| {
}); let from = format!("{:width$}", e.from, width = from_width).into();
ListItem::new(Line::from(vec![from, " ".into(), e.subject.into()]))
})
.collect_vec();
let mut state = ListState::default().with_selected(Some(selected_index)); let mut state = ListState::default().with_selected(Some(selected_index));
StatefulWidget::render( StatefulWidget::render(
List::new(items) List::new(items)
@@ -133,7 +123,7 @@ fn render_email(selected_index: usize, area: Rect, buf: &mut Buffer) {
block.render(area, buf); block.render(area, buf);
if let Some(email) = email { if let Some(email) = email {
let vertical = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]); let vertical = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]);
let [headers_area, body_area] = vertical.areas(inner); let [headers_area, body_area] = inner.split(&vertical);
let headers = vec![ let headers = vec![
Line::from(vec![ Line::from(vec![
"From: ".set_style(theme.header), "From: ".set_style(theme.header),

View File

@@ -1,14 +1,5 @@
use itertools::Itertools; use itertools::Itertools;
use ratatui::{ use ratatui::{prelude::*, widgets::*};
buffer::Buffer,
layout::{Alignment, Constraint, Layout, Margin, Rect},
style::{Style, Stylize},
text::Line,
widgets::{
Block, Clear, Padding, Paragraph, Row, Scrollbar, ScrollbarOrientation, ScrollbarState,
StatefulWidget, Table, TableState, Widget, Wrap,
},
};
use crate::{RgbSwatch, THEME}; use crate::{RgbSwatch, THEME};
@@ -19,7 +10,6 @@ struct Ingredient {
} }
impl Ingredient { impl Ingredient {
#[allow(clippy::cast_possible_truncation)]
fn height(&self) -> u16 { fn height(&self) -> u16 {
self.name.lines().count() as u16 self.name.lines().count() as u16
} }
@@ -94,27 +84,23 @@ const INGREDIENTS: &[Ingredient] = &[
}, },
]; ];
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] #[derive(Debug)]
pub struct RecipeTab { pub struct RecipeTab {
row_index: usize, selected_row: usize,
} }
impl RecipeTab { impl RecipeTab {
/// Select the previous item in the ingredients list (with wrap around) pub fn new(selected_row: usize) -> Self {
pub fn prev(&mut self) { Self {
self.row_index = self.row_index.saturating_add(INGREDIENTS.len() - 1) % INGREDIENTS.len(); selected_row: selected_row % INGREDIENTS.len(),
} }
/// Select the next item in the ingredients list (with wrap around)
pub fn next(&mut self) {
self.row_index = self.row_index.saturating_add(1) % INGREDIENTS.len();
} }
} }
impl Widget for RecipeTab { impl Widget for RecipeTab {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
RgbSwatch.render(area, buf); RgbSwatch.render(area, buf);
let area = area.inner(Margin { let area = area.inner(&Margin {
vertical: 1, vertical: 1,
horizontal: 2, horizontal: 2,
}); });
@@ -131,17 +117,19 @@ impl Widget for RecipeTab {
height: area.height - 3, height: area.height - 3,
..area ..area
}; };
render_scrollbar(self.row_index, scrollbar_area, buf); render_scrollbar(self.selected_row, scrollbar_area, buf);
let area = area.inner(Margin { let area = area.inner(&Margin {
horizontal: 2, horizontal: 2,
vertical: 1, vertical: 1,
}); });
let [recipe, ingredients] = let [recipe, ingredients] = area.split(&Layout::horizontal([
Layout::horizontal([Constraint::Length(44), Constraint::Min(0)]).areas(area); Constraint::Length(44),
Constraint::Min(0),
]));
render_recipe(recipe, buf); render_recipe(recipe, buf);
render_ingredients(self.row_index, ingredients, buf); render_ingredients(self.selected_row, ingredients, buf);
} }
} }
@@ -158,13 +146,13 @@ fn render_recipe(area: Rect, buf: &mut Buffer) {
fn render_ingredients(selected_row: usize, area: Rect, buf: &mut Buffer) { fn render_ingredients(selected_row: usize, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default().with_selected(Some(selected_row)); let mut state = TableState::default().with_selected(Some(selected_row));
let rows = INGREDIENTS.iter().copied(); let rows = INGREDIENTS.iter().cloned();
let theme = THEME.recipe; let theme = THEME.recipe;
StatefulWidget::render( StatefulWidget::render(
Table::new(rows, [Constraint::Length(7), Constraint::Length(30)]) Table::new(rows, [Constraint::Length(7), Constraint::Length(30)])
.block(Block::new().style(theme.ingredients)) .block(Block::new().style(theme.ingredients))
.header(Row::new(vec!["Qty", "Ingredient"]).style(theme.ingredients_header)) .header(Row::new(vec!["Qty", "Ingredient"]).style(theme.ingredients_header))
.row_highlight_style(Style::new().light_yellow()), .highlight_style(Style::new().light_yellow()),
area, area,
buf, buf,
&mut state, &mut state,
@@ -181,5 +169,5 @@ fn render_scrollbar(position: usize, area: Rect, buf: &mut Buffer) {
.end_symbol(None) .end_symbol(None)
.track_symbol(None) .track_symbol(None)
.thumb_symbol("") .thumb_symbol("")
.render(area, buf, &mut state); .render(area, buf, &mut state)
} }

View File

@@ -1,39 +1,28 @@
use itertools::Itertools; use itertools::Itertools;
use ratatui::{ use ratatui::{
buffer::Buffer, prelude::*,
layout::{Alignment, Constraint, Layout, Margin, Rect}, widgets::{canvas::*, *},
style::{Styled, Stylize},
symbols::Marker,
widgets::{
canvas::{self, Canvas, Map, MapResolution, Points},
Block, BorderType, Clear, Padding, Row, Scrollbar, ScrollbarOrientation, ScrollbarState,
Sparkline, StatefulWidget, Table, TableState, Widget,
},
}; };
use crate::{RgbSwatch, THEME}; use crate::{RgbSwatch, THEME};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] #[derive(Debug)]
pub struct TracerouteTab { pub struct TracerouteTab {
row_index: usize, selected_row: usize,
} }
impl TracerouteTab { impl TracerouteTab {
/// Select the previous row (with wrap around). pub fn new(selected_row: usize) -> Self {
pub fn prev_row(&mut self) { Self {
self.row_index = self.row_index.saturating_add(HOPS.len() - 1) % HOPS.len(); selected_row: selected_row % HOPS.len(),
} }
/// Select the next row (with wrap around).
pub fn next_row(&mut self) {
self.row_index = self.row_index.saturating_add(1) % HOPS.len();
} }
} }
impl Widget for TracerouteTab { impl Widget for TracerouteTab {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
RgbSwatch.render(area, buf); RgbSwatch.render(area, buf);
let area = area.inner(Margin { let area = area.inner(&Margin {
vertical: 1, vertical: 1,
horizontal: 2, horizontal: 2,
}); });
@@ -41,26 +30,29 @@ impl Widget for TracerouteTab {
Block::new().style(THEME.content).render(area, buf); Block::new().style(THEME.content).render(area, buf);
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]); let horizontal = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
let vertical = Layout::vertical([Constraint::Min(0), Constraint::Length(3)]); let vertical = Layout::vertical([Constraint::Min(0), Constraint::Length(3)]);
let [left, map] = horizontal.areas(area); let [left, map] = area.split(&horizontal);
let [hops, pings] = vertical.areas(left); let [hops, pings] = left.split(&vertical);
render_hops(self.row_index, hops, buf); render_hops(self.selected_row, hops, buf);
render_ping(self.row_index, pings, buf); render_ping(self.selected_row, pings, buf);
render_map(self.row_index, map, buf); render_map(self.selected_row, map, buf);
} }
} }
fn render_hops(selected_row: usize, area: Rect, buf: &mut Buffer) { fn render_hops(selected_row: usize, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default().with_selected(Some(selected_row)); let mut state = TableState::default().with_selected(Some(selected_row));
let rows = HOPS.iter().map(|hop| Row::new(vec![hop.host, hop.address])); let rows = HOPS
let block = Block::new() .iter()
.padding(Padding::new(1, 1, 1, 1)) .map(|hop| Row::new(vec![hop.host, hop.address]))
.collect_vec();
let block = Block::default()
.title("Traceroute bad.horse".bold().white())
.title_alignment(Alignment::Center) .title_alignment(Alignment::Center)
.title("Traceroute bad.horse".bold().white()); .padding(Padding::new(1, 1, 1, 1));
StatefulWidget::render( StatefulWidget::render(
Table::new(rows, [Constraint::Max(100), Constraint::Length(15)]) Table::new(rows, [Constraint::Max(100), Constraint::Length(15)])
.header(Row::new(vec!["Host", "Address"]).set_style(THEME.traceroute.header)) .header(Row::new(vec!["Host", "Address"]).set_style(THEME.traceroute.header))
.row_highlight_style(THEME.traceroute.selected) .highlight_style(THEME.traceroute.selected)
.block(block), .block(block),
area, area,
buf, buf,
@@ -108,7 +100,7 @@ fn render_map(selected_row: usize, area: Rect, buf: &mut Buffer) {
let theme = THEME.traceroute.map; let theme = THEME.traceroute.map;
let path: Option<(&Hop, &Hop)> = HOPS.iter().tuple_windows().nth(selected_row); let path: Option<(&Hop, &Hop)> = HOPS.iter().tuple_windows().nth(selected_row);
let map = Map { let map = Map {
resolution: MapResolution::High, resolution: canvas::MapResolution::High,
color: theme.color, color: theme.color,
}; };
Canvas::default() Canvas::default()

View File

@@ -1,71 +1,61 @@
use itertools::Itertools; use itertools::Itertools;
use palette::Okhsv; use palette::Okhsv;
use ratatui::{ use ratatui::{
buffer::Buffer, prelude::*,
layout::{Constraint, Direction, Layout, Margin, Rect}, widgets::{calendar::CalendarEventStore, *},
style::{Color, Style, Stylize},
symbols,
widgets::{
calendar::{CalendarEventStore, Monthly},
Bar, BarChart, BarGroup, Block, Clear, LineGauge, Padding, Widget,
},
}; };
use time::OffsetDateTime; use time::OffsetDateTime;
use crate::{color_from_oklab, RgbSwatch, THEME}; use crate::{color_from_oklab, RgbSwatch, THEME};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct WeatherTab { pub struct WeatherTab {
pub download_progress: usize, pub selected_row: usize,
} }
impl WeatherTab { impl WeatherTab {
/// Simulate a download indicator by decrementing the row index. pub fn new(selected_row: usize) -> Self {
pub fn prev(&mut self) { Self { selected_row }
self.download_progress = self.download_progress.saturating_sub(1);
}
/// Simulate a download indicator by incrementing the row index.
pub fn next(&mut self) {
self.download_progress = self.download_progress.saturating_add(1);
} }
} }
impl Widget for WeatherTab { impl Widget for WeatherTab {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
RgbSwatch.render(area, buf); RgbSwatch.render(area, buf);
let area = area.inner(Margin { let area = area.inner(&Margin {
vertical: 1, vertical: 1,
horizontal: 2, horizontal: 2,
}); });
Clear.render(area, buf); Clear.render(area, buf);
Block::new().style(THEME.content).render(area, buf); Block::new().style(THEME.content).render(area, buf);
let area = area.inner(Margin { let area = area.inner(&Margin {
horizontal: 2, horizontal: 2,
vertical: 1, vertical: 1,
}); });
let [main, _, gauges] = Layout::vertical([ let [main, _, gauges] = area.split(&Layout::vertical([
Constraint::Min(0), Constraint::Min(0),
Constraint::Length(1), Constraint::Length(1),
Constraint::Length(1), Constraint::Length(1),
]) ]));
.areas(area); let [calendar, charts] = main.split(&Layout::horizontal([
let [calendar, charts] = Constraint::Length(23),
Layout::horizontal([Constraint::Length(23), Constraint::Min(0)]).areas(main); Constraint::Min(0),
let [simple, horizontal] = ]));
Layout::vertical([Constraint::Length(29), Constraint::Min(0)]).areas(charts); let [simple, horizontal] = charts.split(&Layout::vertical([
Constraint::Length(29),
Constraint::Min(0),
]));
render_calendar(calendar, buf); render_calendar(calendar, buf);
render_simple_barchart(simple, buf); render_simple_barchart(simple, buf);
render_horizontal_barchart(horizontal, buf); render_horizontal_barchart(horizontal, buf);
render_gauge(self.download_progress, gauges, buf); render_gauge(self.selected_row, gauges, buf);
} }
} }
fn render_calendar(area: Rect, buf: &mut Buffer) { fn render_calendar(area: Rect, buf: &mut Buffer) {
let date = OffsetDateTime::now_utc().date(); let date = OffsetDateTime::now_utc().date();
Monthly::new(date, CalendarEventStore::today(Style::new().red().bold())) calendar::Monthly::new(date, CalendarEventStore::today(Style::new().red().bold()))
.block(Block::new().padding(Padding::new(0, 0, 2, 0))) .block(Block::new().padding(Padding::new(0, 0, 2, 0)))
.show_month_header(Style::new().bold()) .show_month_header(Style::new().bold())
.show_weekdays_header(Style::new().italic()) .show_weekdays_header(Style::new().italic())
@@ -88,9 +78,9 @@ fn render_simple_barchart(area: Rect, buf: &mut Buffer) {
Bar::default() Bar::default()
.value(value) .value(value)
// This doesn't actually render correctly as the text is too wide for the bar // This doesn't actually render correctly as the text is too wide for the bar
// See https://github.com/ratatui/ratatui/issues/513 for more info // See https://github.com/ratatui-org/ratatui/issues/513 for more info
// (the demo GIFs hack around this by hacking the calculation in bars.rs) // (the demo GIFs hack around this by hacking the calculation in bars.rs)
.text_value(format!("{value}°")) .text_value(format!("{}°", value))
.style(if value > 70 { .style(if value > 70 {
Style::new().fg(Color::Red) Style::new().fg(Color::Red)
} else { } else {
@@ -134,22 +124,20 @@ fn render_horizontal_barchart(area: Rect, buf: &mut Buffer) {
.render(area, buf); .render(area, buf);
} }
#[allow(clippy::cast_precision_loss)]
pub fn render_gauge(progress: usize, area: Rect, buf: &mut Buffer) { pub fn render_gauge(progress: usize, area: Rect, buf: &mut Buffer) {
let percent = (progress * 3).min(100) as f64; let percent = (progress * 3).min(100) as f64;
render_line_gauge(percent, area, buf); render_line_gauge(percent, area, buf);
} }
#[allow(clippy::cast_possible_truncation)]
fn render_line_gauge(percent: f64, area: Rect, buf: &mut Buffer) { fn render_line_gauge(percent: f64, area: Rect, buf: &mut Buffer) {
// cycle color hue based on the percent for a neat effect yellow -> red // cycle color hue based on the percent for a neat effect yellow -> red
let hue = 90.0 - (percent as f32 * 0.6); let hue = 90.0 - (percent as f32 * 0.6);
let value = Okhsv::max_value(); let value = Okhsv::max_value();
let filled_color = color_from_oklab(hue, Okhsv::max_saturation(), value); let fg = color_from_oklab(hue, Okhsv::max_saturation(), value);
let unfilled_color = color_from_oklab(hue, Okhsv::max_saturation(), value * 0.5); let bg = color_from_oklab(hue, Okhsv::max_saturation(), value * 0.5);
let label = if percent < 100.0 { let label = if percent < 100.0 {
format!("Downloading: {percent}%") format!("Downloading: {}%", percent)
} else { } else {
"Download Complete!".into() "Download Complete!".into()
}; };
@@ -157,8 +145,7 @@ fn render_line_gauge(percent: f64, area: Rect, buf: &mut Buffer) {
.ratio(percent / 100.0) .ratio(percent / 100.0)
.label(label) .label(label)
.style(Style::new().light_blue()) .style(Style::new().light_blue())
.filled_style(Style::new().fg(filled_color)) .gauge_style(Style::new().fg(fg).bg(bg))
.unfilled_style(Style::new().fg(unfilled_color))
.line_set(symbols::line::THICK) .line_set(symbols::line::THICK)
.render(area, buf); .render(area, buf);
} }

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

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

View File

@@ -1,4 +1,4 @@
use ratatui::style::{Color, Modifier, Style}; use ratatui::prelude::*;
pub struct Theme { pub struct Theme {
pub root: Style, pub root: Style,

View File

@@ -1,70 +1,57 @@
//! # [Ratatui] Docs.rs example use std::io::{self, stdout};
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use color_eyre::Result; use crossterm::{
use ratatui::{ terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
crossterm::event::{self, Event, KeyCode}, ExecutableCommand,
layout::{Constraint, Layout},
style::{Color, Modifier, Style, Stylize},
text::{Line, Span, Text},
widgets::{Block, Borders, Paragraph},
DefaultTerminal, Frame,
}; };
use ratatui::{prelude::*, widgets::*};
/// Example code for lib.rs /// Example code for libr.rs
/// ///
/// When cargo-rdme supports doc comments that import from code, this will be imported /// When cargo-rdme supports doc comments that import from code, this will be imported
/// rather than copied to the lib.rs file. /// rather than copied to the lib.rs file.
fn main() -> Result<()> { fn main() -> io::Result<()> {
color_eyre::install()?; let arg = std::env::args().nth(1).unwrap_or_default();
let first_arg = std::env::args().nth(1).unwrap_or_default(); enable_raw_mode()?;
let terminal = ratatui::init(); stdout().execute(EnterAlternateScreen)?;
let app_result = run(terminal, &first_arg); let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
ratatui::restore();
app_result
}
fn run(mut terminal: DefaultTerminal, first_arg: &str) -> Result<()> {
let mut should_quit = false; let mut should_quit = false;
while !should_quit { while !should_quit {
terminal.draw(match first_arg { terminal.draw(match arg.as_str() {
"hello_world" => hello_world,
"layout" => layout, "layout" => layout,
"styling" => styling, "styling" => styling,
_ => hello_world, _ => hello_world,
})?; })?;
should_quit = handle_events()?; should_quit = handle_events()?;
} }
Ok(())
}
fn handle_events() -> std::io::Result<bool> { disable_raw_mode()?;
if let Event::Key(key) = event::read()? { stdout().execute(LeaveAlternateScreen)?;
if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') { Ok(())
return Ok(true);
}
}
Ok(false)
} }
fn hello_world(frame: &mut Frame) { fn hello_world(frame: &mut Frame) {
frame.render_widget( frame.render_widget(
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")), Paragraph::new("Hello World!")
frame.area(), .block(Block::default().title("Greeting").borders(Borders::ALL)),
frame.size(),
); );
} }
use crossterm::event::{self, Event, KeyCode};
fn handle_events() -> io::Result<bool> {
if event::poll(std::time::Duration::from_millis(50))? {
if let Event::Key(key) = event::read()? {
if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(true);
}
}
}
Ok(false)
}
fn layout(frame: &mut Frame) { fn layout(frame: &mut Frame) {
let vertical = Layout::vertical([ let vertical = Layout::vertical([
Constraint::Length(1), Constraint::Length(1),
@@ -72,8 +59,8 @@ fn layout(frame: &mut Frame) {
Constraint::Length(1), Constraint::Length(1),
]); ]);
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2); 2]); let horizontal = Layout::horizontal([Constraint::Ratio(1, 2); 2]);
let [title_bar, main_area, status_bar] = vertical.areas(frame.area()); let [title_bar, main_area, status_bar] = frame.size().split(&vertical);
let [left, right] = horizontal.areas(main_area); let [left, right] = main_area.split(&horizontal);
frame.render_widget( frame.render_widget(
Block::new().borders(Borders::TOP).title("Title Bar"), Block::new().borders(Borders::TOP).title("Title Bar"),
@@ -83,8 +70,8 @@ fn layout(frame: &mut Frame) {
Block::new().borders(Borders::TOP).title("Status Bar"), Block::new().borders(Borders::TOP).title("Status Bar"),
status_bar, status_bar,
); );
frame.render_widget(Block::bordered().title("Left"), left); frame.render_widget(Block::default().borders(Borders::ALL).title("Left"), left);
frame.render_widget(Block::bordered().title("Right"), right); frame.render_widget(Block::default().borders(Borders::ALL).title("Right"), right);
} }
fn styling(frame: &mut Frame) { fn styling(frame: &mut Frame) {
@@ -95,7 +82,7 @@ fn styling(frame: &mut Frame) {
Constraint::Length(1), Constraint::Length(1),
Constraint::Min(0), Constraint::Min(0),
]) ])
.split(frame.area()); .split(frame.size());
let span1 = Span::raw("Hello "); let span1 = Span::raw("Hello ");
let span2 = Span::styled( let span2 = Span::styled(

View File

@@ -1,5 +1,5 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info. # This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/docsrs.tape` # To run this script, install vhs and run `vhs ./examples/demo.tape`
# NOTE: Requires VHS 0.6.1 or later for Screenshot support # NOTE: Requires VHS 0.6.1 or later for Screenshot support
Output "target/docsrs.gif" Output "target/docsrs.gif"
Set Theme "Aardvark Blue" Set Theme "Aardvark Blue"

View File

@@ -1,119 +1,57 @@
//! # [Ratatui] Flex example use std::io::{self, stdout};
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use std::num::NonZeroUsize; use color_eyre::{config::HookBuilder, Result};
use crossterm::{
use color_eyre::Result; event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{ use ratatui::{
buffer::Buffer, layout::{Constraint::*, Flex},
crossterm::event::{self, Event, KeyCode, KeyEventKind}, prelude::*,
layout::{ style::palette::tailwind,
Alignment, widgets::{block::Title, *},
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
Flex, Layout, Rect,
},
style::{palette::tailwind, Color, Modifier, Style, Stylize},
symbols::{self, line},
text::{Line, Text},
widgets::{
Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Tabs,
Widget,
},
DefaultTerminal,
}; };
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator}; use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::default().run(terminal);
ratatui::restore();
app_result
}
const EXAMPLE_DATA: &[(&str, &[Constraint])] = &[ const EXAMPLE_DATA: &[(&str, &[Constraint])] = &[
( (
"Min(u16) takes any excess space always", "Min(u16) takes any excess space when using `Stretch` or `StretchLast`",
&[Length(10), Min(10), Max(10), Percentage(10), Ratio(1,10)], &[Fixed(20), Min(20), Max(20)],
), ),
( (
"Fill(u16) takes any excess space always", "Proportional(u16) takes any excess space in all `Flex` layouts",
&[Length(20), Percentage(20), Ratio(1, 5), Fill(1)], &[Length(20), Percentage(20), Ratio(1, 5), Proportional(1)],
), ),
( (
"Here's all constraints in one line", "In `StretchLast`, last constraint of lowest priority takes excess space",
&[Length(10), Min(10), Max(10), Percentage(10), Ratio(1,10), Fill(1)], &[Length(20), Fixed(20), Percentage(20)],
), ),
("", &[Fixed(20), Percentage(20), Length(20)]),
("", &[Percentage(20), Length(20), Fixed(20)]),
("", &[Length(20), Length(15)]),
("Spacing has no effect in `SpaceAround` and `SpaceBetween`", &[Proportional(1), Proportional(1)]),
("", &[Length(20), Fixed(20)]),
( (
"", "When not using `Flex::Stretch` or `Flex::StretchLast`,\n`Min(u16)` and `Max(u16)` collapse to their lowest values",
&[Max(50), Min(50)],
),
(
"",
&[Max(20), Length(10)],
),
(
"",
&[Max(20), Length(10)],
),
(
"Min grows always but also allows Fill to grow",
&[Percentage(50), Fill(1), Fill(2), Min(50)],
),
(
"In `Legacy`, the last constraint of lowest priority takes excess space",
&[Length(20), Length(20), Percentage(20)],
),
("", &[Length(20), Percentage(20), Length(20)]),
("A lowest priority constraint will be broken before a high priority constraint", &[Ratio(1,4), Percentage(20)]),
("`Length` is higher priority than `Percentage`", &[Percentage(20), Length(10)]),
("`Min/Max` is higher priority than `Length`", &[Length(10), Max(20)]),
("", &[Length(100), Min(20)]),
("`Length` is higher priority than `Min/Max`", &[Max(20), Length(10)]),
("", &[Min(20), Length(90)]),
("Fill is the lowest priority and will fill any excess space", &[Fill(1), Ratio(1, 4)]),
("Fill can be used to scale proportionally with other Fill blocks", &[Fill(1), Percentage(20), Fill(2)]),
("", &[Ratio(1, 3), Percentage(20), Ratio(2, 3)]),
("Legacy will stretch the last lowest priority constraint\nStretch will only stretch equal weighted constraints", &[Length(20), Length(15)]),
("", &[Percentage(20), Length(15)]),
("`Fill(u16)` fills up excess space, but is lower priority to spacers.\ni.e. Fill will only have widths in Flex::Stretch and Flex::Legacy", &[Fill(1), Fill(1)]),
("", &[Length(20), Length(20)]),
(
"When not using `Flex::Stretch` or `Flex::Legacy`,\n`Min(u16)` and `Max(u16)` collapse to their lowest values",
&[Min(20), Max(20)], &[Min(20), Max(20)],
), ),
( (
"", "`SpaceBetween` stretches when there's only one constraint",
&[Max(20)], &[Max(20)],
), ),
("", &[Min(20), Max(20), Length(20), Length(20)]), ("", &[Min(20), Max(20), Length(20), Fixed(20)]),
("", &[Fill(0), Fill(0)]), ("`Proportional(u16)` always fills up space in every `Flex` layout", &[Proportional(0), Proportional(0)]),
( (
"`Fill(1)` can be to scale with respect to other `Fill(2)`", "`Proportional(1)` can be to scale with respect to other `Proportional(2)`",
&[Fill(1), Fill(2)], &[Proportional(1), Proportional(2)],
), ),
( (
"", "`Proportional(0)` collapses if there are other non-zero `Proportional(_)`\nconstraints. e.g. `[Proportional(0), Proportional(0), Proportional(1)]`:",
&[Fill(1), Min(10), Max(10), Fill(2)],
),
(
"`Fill(0)` collapses if there are other non-zero `Fill(_)`\nconstraints. e.g. `[Fill(0), Fill(0), Fill(1)]`:",
&[ &[
Fill(0), Proportional(0),
Fill(0), Proportional(0),
Fill(1), Proportional(1),
], ],
), ),
]; ];
@@ -149,7 +87,8 @@ struct Example {
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, FromRepr, Display, EnumIter)] #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, FromRepr, Display, EnumIter)]
enum SelectedTab { enum SelectedTab {
#[default] #[default]
Legacy, StretchLast,
Stretch,
Start, Start,
Center, Center,
End, End,
@@ -157,38 +96,55 @@ enum SelectedTab {
SpaceBetween, SpaceBetween,
} }
impl App { fn main() -> Result<()> {
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { // assuming the user changes spacing about a 100 times or so
// increase the layout cache to account for the number of layout events. This ensures that Layout::init_cache(EXAMPLE_DATA.len() * SelectedTab::iter().len() * 100);
// layout is not generally reprocessed on every frame (which would lead to possible janky init_error_hooks()?;
// results when there are more than one possible solution to the requested layout). This let terminal = init_terminal()?;
// assumes the user changes spacing about a 100 times or so.
let cache_size = EXAMPLE_DATA.len() * SelectedTab::iter().len() * 100;
Layout::init_cache(NonZeroUsize::new(cache_size).unwrap());
// Each line in the example is a layout
// so 13 examples * 7 = 91 currently
// Plus additional layout for tabs ...
Layout::init_cache(120);
App::default().run(terminal)?;
restore_terminal()?;
Ok(())
}
impl App {
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
self.draw(&mut terminal)?;
while self.is_running() { while self.is_running() {
terminal.draw(|frame| frame.render_widget(self, frame.area()))?;
self.handle_events()?; self.handle_events()?;
self.draw(&mut terminal)?;
} }
Ok(()) Ok(())
} }
fn is_running(self) -> bool { fn is_running(&self) -> bool {
self.state == AppState::Running self.state == AppState::Running
} }
fn draw(self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
Ok(())
}
fn handle_events(&mut self) -> Result<()> { fn handle_events(&mut self) -> Result<()> {
use KeyCode::*;
match event::read()? { match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code { Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
KeyCode::Char('q') | KeyCode::Esc => self.quit(), Char('q') | Esc => self.quit(),
KeyCode::Char('l') | KeyCode::Right => self.next(), Char('l') | Right => self.next(),
KeyCode::Char('h') | KeyCode::Left => self.previous(), Char('h') | Left => self.previous(),
KeyCode::Char('j') | KeyCode::Down => self.down(), Char('j') | Down => self.down(),
KeyCode::Char('k') | KeyCode::Up => self.up(), Char('k') | Up => self.up(),
KeyCode::Char('g') | KeyCode::Home => self.top(), Char('g') | Home => self.top(),
KeyCode::Char('G') | KeyCode::End => self.bottom(), Char('G') | End => self.bottom(),
KeyCode::Char('+') => self.increment_spacing(), Char('+') => self.increment_spacing(),
KeyCode::Char('-') => self.decrement_spacing(), Char('-') => self.decrement_spacing(),
_ => (), _ => (),
}, },
_ => {} _ => {}
@@ -205,14 +161,14 @@ impl App {
} }
fn up(&mut self) { fn up(&mut self) {
self.scroll_offset = self.scroll_offset.saturating_sub(1); self.scroll_offset = self.scroll_offset.saturating_sub(1)
} }
fn down(&mut self) { fn down(&mut self) {
self.scroll_offset = self self.scroll_offset = self
.scroll_offset .scroll_offset
.saturating_add(1) .saturating_add(1)
.min(max_scroll_offset()); .min(max_scroll_offset())
} }
fn top(&mut self) { fn top(&mut self) {
@@ -241,7 +197,8 @@ fn max_scroll_offset() -> u16 {
example_height() example_height()
- EXAMPLE_DATA - EXAMPLE_DATA
.last() .last()
.map_or(0, |(desc, _)| get_description_height(desc) + 4) .map(|(desc, _)| get_description_height(desc) + 4)
.unwrap_or(0)
} }
/// The height of all examples combined /// The height of all examples combined
@@ -256,24 +213,24 @@ fn example_height() -> u16 {
impl Widget for App { impl Widget for App {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let layout = Layout::vertical([Length(3), Length(1), Fill(0)]); let layout = Layout::vertical([Fixed(3), Fixed(1), Proportional(0)]);
let [tabs, axis, demo] = layout.areas(area); let [tabs, axis, demo] = area.split(&layout);
self.tabs().render(tabs, buf); self.tabs().render(tabs, buf);
let scroll_needed = self.render_demo(demo, buf); let scroll_needed = self.render_demo(demo, buf);
let axis_width = if scroll_needed { let axis_width = if scroll_needed {
axis.width.saturating_sub(1) axis.width - 1
} else { } else {
axis.width axis.width
}; };
Self::axis(axis_width, self.spacing).render(axis, buf); self.axis(axis_width, self.spacing).render(axis, buf);
} }
} }
impl App { impl App {
fn tabs(self) -> impl Widget { fn tabs(&self) -> impl Widget {
let tab_titles = SelectedTab::iter().map(SelectedTab::to_tab_title); let tab_titles = SelectedTab::iter().map(SelectedTab::to_tab_title);
let block = Block::new() let block = Block::new()
.title("Flex Layouts ".bold()) .title(Title::from("Flex Layouts ".bold()))
.title(" Use ◄ ► to change tab, ▲ ▼ to scroll, - + to change spacing "); .title(" Use ◄ ► to change tab, ▲ ▼ to scroll, - + to change spacing ");
Tabs::new(tab_titles) Tabs::new(tab_titles)
.block(block) .block(block)
@@ -284,17 +241,17 @@ impl App {
} }
/// a bar like `<----- 80 px (gap: 2 px)? ----->` /// a bar like `<----- 80 px (gap: 2 px)? ----->`
fn axis(width: u16, spacing: u16) -> impl Widget { fn axis(&self, width: u16, spacing: u16) -> impl Widget {
let width = width as usize; let width = width as usize;
// only show gap when spacing is not zero // only show gap when spacing is not zero
let label = if spacing != 0 { let label = if spacing != 0 {
format!("{width} px (gap: {spacing} px)") format!("{} px (gap: {} px)", width, spacing)
} else { } else {
format!("{width} px") format!("{} px", width)
}; };
let bar_width = width.saturating_sub(2); // we want to `<` and `>` at the ends let bar_width = width - 2; // we want to `<` and `>` at the ends
let width_bar = format!("<{label:-^bar_width$}>"); let width_bar = format!("<{label:-^bar_width$}>");
Paragraph::new(width_bar.dark_gray()).centered() Paragraph::new(width_bar.dark_gray()).alignment(Alignment::Center)
} }
/// Render the demo content /// Render the demo content
@@ -303,7 +260,6 @@ impl App {
/// into the main buffer. This is done to make it possible to handle scrolling easily. /// into the main buffer. This is done to make it possible to handle scrolling easily.
/// ///
/// Returns bool indicating whether scroll was needed /// Returns bool indicating whether scroll was needed
#[allow(clippy::cast_possible_truncation)]
fn render_demo(self, area: Rect, buf: &mut Buffer) -> bool { fn render_demo(self, area: Rect, buf: &mut Buffer) -> bool {
// render demo content into a separate buffer so all examples fit we add an extra // render demo content into a separate buffer so all examples fit we add an extra
// area.height to make sure the last example is fully visible even when the scroll offset is // area.height to make sure the last example is fully visible even when the scroll offset is
@@ -334,7 +290,7 @@ impl App {
for (i, cell) in visible_content.enumerate() { for (i, cell) in visible_content.enumerate() {
let x = i as u16 % area.width; let x = i as u16 % area.width;
let y = i as u16 / area.width; let y = i as u16 / area.width;
buf[(area.x + x, area.y + y)] = cell; *buf.get_mut(area.x + x, area.y + y) = cell;
} }
if scrollbar_needed { if scrollbar_needed {
@@ -349,30 +305,32 @@ impl App {
impl SelectedTab { impl SelectedTab {
/// Get the previous tab, if there is no previous tab return the current tab. /// Get the previous tab, if there is no previous tab return the current tab.
fn previous(self) -> Self { fn previous(&self) -> Self {
let current_index: usize = self as usize; let current_index: usize = *self as usize;
let previous_index = current_index.saturating_sub(1); let previous_index = current_index.saturating_sub(1);
Self::from_repr(previous_index).unwrap_or(self) Self::from_repr(previous_index).unwrap_or(*self)
} }
/// Get the next tab, if there is no next tab return the current tab. /// Get the next tab, if there is no next tab return the current tab.
fn next(self) -> Self { fn next(&self) -> Self {
let current_index = self as usize; let current_index = *self as usize;
let next_index = current_index.saturating_add(1); let next_index = current_index.saturating_add(1);
Self::from_repr(next_index).unwrap_or(self) Self::from_repr(next_index).unwrap_or(*self)
} }
/// Convert a `SelectedTab` into a `Line` to display it by the `Tabs` widget. /// Convert a `SelectedTab` into a `Line` to display it by the `Tabs` widget.
fn to_tab_title(value: Self) -> Line<'static> { fn to_tab_title(value: SelectedTab) -> Line<'static> {
use tailwind::{INDIGO, ORANGE, SKY}; use tailwind::*;
use SelectedTab::*;
let text = value.to_string(); let text = value.to_string();
let color = match value { let color = match value {
Self::Legacy => ORANGE.c400, StretchLast => ORANGE.c400,
Self::Start => SKY.c400, Stretch => ORANGE.c300,
Self::Center => SKY.c300, Start => SKY.c400,
Self::End => SKY.c200, Center => SKY.c300,
Self::SpaceAround => INDIGO.c400, End => SKY.c200,
Self::SpaceBetween => INDIGO.c300, SpaceAround => INDIGO.c400,
SpaceBetween => INDIGO.c300,
}; };
format!(" {text} ").fg(color).bg(Color::Black).into() format!(" {text} ").fg(color).bg(Color::Black).into()
} }
@@ -383,18 +341,21 @@ impl StatefulWidget for SelectedTab {
fn render(self, area: Rect, buf: &mut Buffer, spacing: &mut Self::State) { fn render(self, area: Rect, buf: &mut Buffer, spacing: &mut Self::State) {
let spacing = *spacing; let spacing = *spacing;
match self { match self {
Self::Legacy => Self::render_examples(area, buf, Flex::Legacy, spacing), SelectedTab::StretchLast => self.render_examples(area, buf, Flex::StretchLast, spacing),
Self::Start => Self::render_examples(area, buf, Flex::Start, spacing), SelectedTab::Stretch => self.render_examples(area, buf, Flex::Stretch, spacing),
Self::Center => Self::render_examples(area, buf, Flex::Center, spacing), SelectedTab::Start => self.render_examples(area, buf, Flex::Start, spacing),
Self::End => Self::render_examples(area, buf, Flex::End, spacing), SelectedTab::Center => self.render_examples(area, buf, Flex::Center, spacing),
Self::SpaceAround => Self::render_examples(area, buf, Flex::SpaceAround, spacing), SelectedTab::End => self.render_examples(area, buf, Flex::End, spacing),
Self::SpaceBetween => Self::render_examples(area, buf, Flex::SpaceBetween, spacing), SelectedTab::SpaceAround => self.render_examples(area, buf, Flex::SpaceAround, spacing),
SelectedTab::SpaceBetween => {
self.render_examples(area, buf, Flex::SpaceBetween, spacing)
}
} }
} }
} }
impl SelectedTab { impl SelectedTab {
fn render_examples(area: Rect, buf: &mut Buffer, flex: Flex, spacing: u16) { fn render_examples(&self, area: Rect, buf: &mut Buffer, flex: Flex, spacing: u16) {
let heights = EXAMPLE_DATA let heights = EXAMPLE_DATA
.iter() .iter()
.map(|(desc, _)| get_description_height(desc) + 4); .map(|(desc, _)| get_description_height(desc) + 4);
@@ -419,19 +380,18 @@ impl Example {
impl Widget for Example { impl Widget for Example {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let title_height = get_description_height(&self.description); let title_height = get_description_height(&self.description);
let layout = Layout::vertical([Length(title_height), Fill(0)]); let layout = Layout::vertical([Fixed(title_height), Proportional(0)]);
let [title, illustrations] = layout.areas(area); let [title, illustrations] = area.split(&layout);
let blocks = Layout::horizontal(&self.constraints)
let (blocks, spacers) = Layout::horizontal(&self.constraints)
.flex(self.flex) .flex(self.flex)
.spacing(self.spacing) .spacing(self.spacing)
.split_with_spacers(illustrations); .split(illustrations);
if !self.description.is_empty() { if !self.description.is_empty() {
Paragraph::new( Paragraph::new(
self.description self.description
.split('\n') .split('\n')
.map(|s| format!("// {s}").italic().fg(tailwind::SLATE.c400)) .map(|s| format!("// {}", s).italic().fg(tailwind::SLATE.c400))
.map(Line::from) .map(Line::from)
.collect::<Vec<Line>>(), .collect::<Vec<Line>>(),
) )
@@ -439,62 +399,14 @@ impl Widget for Example {
} }
for (block, constraint) in blocks.iter().zip(&self.constraints) { for (block, constraint) in blocks.iter().zip(&self.constraints) {
Self::illustration(*constraint, block.width).render(*block, buf); self.illustration(*constraint, block.width)
} .render(*block, buf);
for spacer in spacers.iter() {
Self::render_spacer(*spacer, buf);
} }
} }
} }
impl Example { impl Example {
fn render_spacer(spacer: Rect, buf: &mut Buffer) { fn illustration(&self, constraint: Constraint, width: u16) -> Paragraph {
if spacer.width > 1 {
let corners_only = symbols::border::Set {
top_left: line::NORMAL.top_left,
top_right: line::NORMAL.top_right,
bottom_left: line::NORMAL.bottom_left,
bottom_right: line::NORMAL.bottom_right,
vertical_left: " ",
vertical_right: " ",
horizontal_top: " ",
horizontal_bottom: " ",
};
Block::bordered()
.border_set(corners_only)
.border_style(Style::reset().dark_gray())
.render(spacer, buf);
} else {
Paragraph::new(Text::from(vec![
Line::from(""),
Line::from(""),
Line::from(""),
Line::from(""),
]))
.style(Style::reset().dark_gray())
.render(spacer, buf);
}
let width = spacer.width;
let label = if width > 4 {
format!("{width} px")
} else if width > 2 {
format!("{width}")
} else {
String::new()
};
let text = Text::from(vec![
Line::raw(""),
Line::raw(""),
Line::styled(label, Style::reset().dark_gray()),
]);
Paragraph::new(text)
.style(Style::reset().dark_gray())
.alignment(Alignment::Center)
.render(spacer, buf);
}
fn illustration(constraint: Constraint, width: u16) -> impl Widget {
let main_color = color_for_constraint(constraint); let main_color = color_for_constraint(constraint);
let fg_color = Color::White; let fg_color = Color::White;
let title = format!("{constraint}"); let title = format!("{constraint}");
@@ -504,23 +416,54 @@ impl Example {
.border_set(symbols::border::QUADRANT_OUTSIDE) .border_set(symbols::border::QUADRANT_OUTSIDE)
.border_style(Style::reset().fg(main_color).reversed()) .border_style(Style::reset().fg(main_color).reversed())
.style(Style::default().fg(fg_color).bg(main_color)); .style(Style::default().fg(fg_color).bg(main_color));
Paragraph::new(text).centered().block(block) Paragraph::new(text)
.alignment(Alignment::Center)
.block(block)
} }
} }
const fn color_for_constraint(constraint: Constraint) -> Color { fn color_for_constraint(constraint: Constraint) -> Color {
use tailwind::{BLUE, SLATE}; use tailwind::*;
match constraint { match constraint {
Constraint::Fixed(_) => RED.c900,
Constraint::Min(_) => BLUE.c900, Constraint::Min(_) => BLUE.c900,
Constraint::Max(_) => BLUE.c800, Constraint::Max(_) => BLUE.c800,
Constraint::Length(_) => SLATE.c700, Constraint::Length(_) => SLATE.c700,
Constraint::Percentage(_) => SLATE.c800, Constraint::Percentage(_) => SLATE.c800,
Constraint::Ratio(_, _) => SLATE.c900, Constraint::Ratio(_, _) => SLATE.c900,
Constraint::Fill(_) => SLATE.c950, Constraint::Proportional(_) => SLATE.c950,
} }
} }
#[allow(clippy::cast_possible_truncation)] fn init_error_hooks() -> Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info)
}));
Ok(())
}
fn init_terminal() -> Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal() -> Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
fn get_description_height(s: &str) -> u16 { fn get_description_height(s: &str) -> u16 {
if s.is_empty() { if s.is_empty() {
0 0

View File

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

View File

@@ -1,100 +1,94 @@
//! # [Ratatui] Gauge example use std::{
//! io::{self, stdout, Stdout},
//! The latest version of this example is available in the [examples] folder in the repository. time::Duration,
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use std::time::Duration;
use color_eyre::Result;
use ratatui::{
buffer::Buffer,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Alignment, Constraint, Layout, Rect},
style::{palette::tailwind, Color, Style, Stylize},
text::{Line, Span},
widgets::{Block, Borders, Gauge, Padding, Paragraph, Widget},
DefaultTerminal,
}; };
const GAUGE1_COLOR: Color = tailwind::RED.c800; use anyhow::Result;
const GAUGE2_COLOR: Color = tailwind::GREEN.c800; use crossterm::{
const GAUGE3_COLOR: Color = tailwind::BLUE.c800; event::{self, Event, KeyCode, KeyEventKind},
const GAUGE4_COLOR: Color = tailwind::ORANGE.c800; terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
const CUSTOM_LABEL_COLOR: Color = tailwind::SLATE.c200; ExecutableCommand,
};
#[derive(Debug, Default, Clone, Copy)] use ratatui::{
struct App { prelude::*,
state: AppState, widgets::{block::Title, *},
progress_columns: u16, };
progress1: u16,
progress2: f64,
progress3: f64,
progress4: f64,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum AppState {
#[default]
Running,
Started,
Quitting,
}
fn main() -> Result<()> { fn main() -> Result<()> {
color_eyre::install()?; App::run()
let terminal = ratatui::init(); }
let app_result = App::default().run(terminal);
ratatui::restore(); struct App {
app_result term: Term,
should_quit: bool,
state: AppState,
}
#[derive(Debug, Default, Clone, Copy)]
struct AppState {
progress1: u16,
progress2: u16,
progress3: f64,
progress4: u16,
} }
impl App { impl App {
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { fn run() -> Result<()> {
while self.state != AppState::Quitting { // run at ~10 fps minus the time it takes to draw
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?; let timeout = Duration::from_secs_f32(1.0 / 10.0);
self.handle_events()?; let mut app = Self::start()?;
self.update(terminal.size()?.width); while !app.should_quit {
app.update();
app.draw()?;
app.handle_events(timeout)?;
} }
app.stop()?;
Ok(()) Ok(())
} }
fn update(&mut self, terminal_width: u16) { fn start() -> Result<Self> {
if self.state != AppState::Started { Ok(App {
return; term: Term::start()?,
} should_quit: false,
state: AppState {
// progress1 and progress2 help show the difference between ratio and percentage measuring progress1: 0,
// the same thing, but converting to either a u16 or f64. Effectively, we're showing the progress2: 0,
// difference between how a continuous gauge acts for floor and rounded values. progress3: 0.0,
self.progress_columns = (self.progress_columns + 1).clamp(0, terminal_width); progress4: 0,
self.progress1 = self.progress_columns * 100 / terminal_width; },
self.progress2 = f64::from(self.progress_columns) * 100.0 / f64::from(terminal_width); })
// progress3 and progress4 similarly show the difference between unicode and non-unicode
// gauges measuring the same thing.
self.progress3 = (self.progress3 + 0.1).clamp(40.0, 100.0);
self.progress4 = (self.progress4 + 0.1).clamp(40.0, 100.0);
} }
fn handle_events(&mut self) -> Result<()> { fn stop(&mut self) -> Result<()> {
let timeout = Duration::from_secs_f32(1.0 / 20.0); Term::stop()?;
Ok(())
}
fn update(&mut self) {
self.state.progress1 = (self.state.progress1 + 4).min(100);
self.state.progress2 = (self.state.progress2 + 3).min(100);
self.state.progress3 = (self.state.progress3 + 0.02).min(1.0);
self.state.progress4 = (self.state.progress4 + 1).min(100);
}
fn draw(&mut self) -> Result<()> {
self.term.draw(|frame| {
let state = self.state;
let layout = Layout::vertical([Constraint::Ratio(1, 4); 4]).split(frame.size());
Self::render_gauge1(state.progress1, frame, layout[0]);
Self::render_gauge2(state.progress2, frame, layout[1]);
Self::render_gauge3(state.progress3, frame, layout[2]);
Self::render_gauge4(state.progress4, frame, layout[3]);
})?;
Ok(())
}
fn handle_events(&mut self, timeout: Duration) -> io::Result<()> {
if event::poll(timeout)? { if event::poll(timeout)? {
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press { if key.kind == KeyEventKind::Press {
match key.code { if let KeyCode::Char('q') = key.code {
KeyCode::Char(' ') | KeyCode::Enter => self.start(), self.should_quit = true;
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
_ => {}
} }
} }
} }
@@ -102,104 +96,79 @@ impl App {
Ok(()) Ok(())
} }
fn start(&mut self) { fn render_gauge1(progress: u16, frame: &mut Frame, area: Rect) {
self.state = AppState::Started; let title = Self::title_block("Gauge with percentage progress");
} let gauge = Gauge::default()
fn quit(&mut self) {
self.state = AppState::Quitting;
}
}
impl Widget for &App {
#[allow(clippy::similar_names)]
fn render(self, area: Rect, buf: &mut Buffer) {
use Constraint::{Length, Min, Ratio};
let layout = Layout::vertical([Length(2), Min(0), Length(1)]);
let [header_area, gauge_area, footer_area] = layout.areas(area);
let layout = Layout::vertical([Ratio(1, 4); 4]);
let [gauge1_area, gauge2_area, gauge3_area, gauge4_area] = layout.areas(gauge_area);
render_header(header_area, buf);
render_footer(footer_area, buf);
self.render_gauge1(gauge1_area, buf);
self.render_gauge2(gauge2_area, buf);
self.render_gauge3(gauge3_area, buf);
self.render_gauge4(gauge4_area, buf);
}
}
fn render_header(area: Rect, buf: &mut Buffer) {
Paragraph::new("Ratatui Gauge Example")
.bold()
.alignment(Alignment::Center)
.fg(CUSTOM_LABEL_COLOR)
.render(area, buf);
}
fn render_footer(area: Rect, buf: &mut Buffer) {
Paragraph::new("Press ENTER to start")
.alignment(Alignment::Center)
.fg(CUSTOM_LABEL_COLOR)
.bold()
.render(area, buf);
}
impl App {
fn render_gauge1(&self, area: Rect, buf: &mut Buffer) {
let title = title_block("Gauge with percentage");
Gauge::default()
.block(title) .block(title)
.gauge_style(GAUGE1_COLOR) .gauge_style(Style::new().light_red())
.percent(self.progress1) .percent(progress);
.render(area, buf); frame.render_widget(gauge, area);
} }
fn render_gauge2(&self, area: Rect, buf: &mut Buffer) { fn render_gauge2(progress: u16, frame: &mut Frame, area: Rect) {
let title = title_block("Gauge with ratio and custom label"); let title = Self::title_block("Gauge with percentage progress and custom label");
let label = format!("{}/100", progress);
let gauge = Gauge::default()
.block(title)
.gauge_style(Style::new().blue().on_light_blue())
.percent(progress)
.label(label);
frame.render_widget(gauge, area);
}
fn render_gauge3(progress: f64, frame: &mut Frame, area: Rect) {
let title =
Self::title_block("Gauge with ratio progress, custom label with style, and unicode");
let label = Span::styled( let label = Span::styled(
format!("{:.1}/100", self.progress2), format!("{:.2}%", progress * 100.0),
Style::new().italic().bold().fg(CUSTOM_LABEL_COLOR), Style::new().red().italic().bold(),
); );
Gauge::default() let gauge = Gauge::default()
.block(title) .block(title)
.gauge_style(GAUGE2_COLOR) .gauge_style(Style::default().fg(Color::Yellow))
.ratio(self.progress2 / 100.0) .ratio(progress)
.label(label) .label(label)
.render(area, buf); .use_unicode(true);
frame.render_widget(gauge, area);
} }
fn render_gauge3(&self, area: Rect, buf: &mut Buffer) { fn render_gauge4(progress: u16, frame: &mut Frame, area: Rect) {
let title = title_block("Gauge with ratio (no unicode)"); let title = Self::title_block("Gauge with percentage progress and label");
let label = format!("{:.1}%", self.progress3); let label = format!("{}/100", progress);
Gauge::default() let gauge = Gauge::default()
.block(title) .block(title)
.gauge_style(GAUGE3_COLOR) .gauge_style(Style::new().green().italic())
.ratio(self.progress3 / 100.0) .percent(progress)
.label(label) .label(label);
.render(area, buf); frame.render_widget(gauge, area);
} }
fn render_gauge4(&self, area: Rect, buf: &mut Buffer) { fn title_block(title: &str) -> Block {
let title = title_block("Gauge with ratio (unicode)"); let title = Title::from(title).alignment(Alignment::Center);
let label = format!("{:.1}%", self.progress3); Block::default().title(title).borders(Borders::TOP)
Gauge::default()
.block(title)
.gauge_style(GAUGE4_COLOR)
.ratio(self.progress4 / 100.0)
.label(label)
.use_unicode(true)
.render(area, buf);
} }
} }
fn title_block(title: &str) -> Block { struct Term {
let title = Line::from(title).centered(); terminal: Terminal<CrosstermBackend<Stdout>>,
Block::new() }
.borders(Borders::NONE)
.padding(Padding::vertical(1)) impl Term {
.title(title) pub fn start() -> io::Result<Term> {
.fg(CUSTOM_LABEL_COLOR) stdout().execute(EnterAlternateScreen)?;
enable_raw_mode()?;
let terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
Ok(Self { terminal })
}
pub fn stop() -> io::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
fn draw(&mut self, frame: impl FnOnce(&mut Frame)) -> Result<()> {
self.terminal.draw(frame)?;
Ok(())
}
} }

View File

@@ -3,12 +3,10 @@
Output "target/gauge.gif" Output "target/gauge.gif"
Set Theme "Aardvark Blue" Set Theme "Aardvark Blue"
Set Width 1200 Set Width 1200
Set Height 850 Set Height 550
Hide Hide
Type "cargo run --example=gauge --features=crossterm" Type "cargo run --example=gauge --features=crossterm"
Enter Enter
Sleep 2s Sleep 1s
Show Show
Sleep 2s Sleep 20s
Enter 1
Sleep 15s

View File

@@ -25,12 +25,12 @@ set -o pipefail
# ensure that running each example doesn't have to wait for the build # ensure that running each example doesn't have to wait for the build
cargo build --examples --features=crossterm,all-widgets cargo build --examples --features=crossterm,all-widgets
for tape_path in examples/vhs/*.tape; do for tape in examples/*.tape; do
tape_file=${tape_path/examples\/vhs\//} # strip the examples/vhs/ prefix gif=${tape/examples\//}
gif_file=${tape_file/.tape/.gif} # replace the .tape suffix with .gif gif=${gif/.tape/.gif}
~/go/bin/vhs $tape_path --quiet ~/go/bin/vhs $tape --quiet
# this can be pasted into the examples README.md # this can be pasted into the examples README.md
echo "[${gif_file}]: https://github.com/ratatui/ratatui/blob/images/examples/${gif_file}?raw=true" echo "[${gif}]: https://github.com/ratatui-org/ratatui/blob/images/examples/${gif}?raw=true"
done done
git switch images git switch images
git pull --rebase upstream images git pull --rebase upstream images

View File

@@ -1,48 +1,57 @@
//! # [Ratatui] Hello World example use std::{
//! io::{self, Stdout},
//! The latest version of this example is available in the [examples] folder in the repository. time::Duration,
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use std::time::Duration;
use color_eyre::{eyre::Context, Result};
use ratatui::{
crossterm::event::{self, Event, KeyCode},
widgets::Paragraph,
DefaultTerminal, Frame,
}; };
use anyhow::{Context, Result};
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
/// This is a bare minimum example. There are many approaches to running an application loop, so /// This is a bare minimum example. There are many approaches to running an application loop, so
/// this is not meant to be prescriptive. It is only meant to demonstrate the basic setup and /// this is not meant to be prescriptive. It is only meant to demonstrate the basic setup and
/// teardown of a terminal application. /// teardown of a terminal application.
/// ///
/// This example does not handle events or update the application state. It just draws a greeting /// A more robust application would probably want to handle errors and ensure that the terminal is
/// and exits when the user presses 'q'. /// restored to a sane state before exiting. This example does not do that. It also does not handle
/// events or update the application state. It just draws a greeting and exits when the user
/// presses 'q'.
fn main() -> Result<()> { fn main() -> Result<()> {
color_eyre::install()?; // augment errors / panics with easy to read messages let mut terminal = setup_terminal().context("setup failed")?;
let terminal = ratatui::init(); run(&mut terminal).context("app loop failed")?;
let app_result = run(terminal).context("app loop failed"); restore_terminal(&mut terminal).context("restore terminal failed")?;
ratatui::restore(); Ok(())
app_result }
/// Setup the terminal. This is where you would enable raw mode, enter the alternate screen, and
/// hide the cursor. This example does not handle errors. A more robust application would probably
/// want to handle errors and ensure that the terminal is restored to a sane state before exiting.
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
let mut stdout = io::stdout();
enable_raw_mode().context("failed to enable raw mode")?;
execute!(stdout, EnterAlternateScreen).context("unable to enter alternate screen")?;
Terminal::new(CrosstermBackend::new(stdout)).context("creating terminal failed")
}
/// Restore the terminal. This is where you disable raw mode, leave the alternate screen, and show
/// the cursor.
fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
disable_raw_mode().context("failed to disable raw mode")?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)
.context("unable to switch to main screen")?;
terminal.show_cursor().context("unable to show cursor")
} }
/// Run the application loop. This is where you would handle events and update the application /// Run the application loop. This is where you would handle events and update the application
/// state. This example exits when the user presses 'q'. Other styles of application loops are /// state. This example exits when the user presses 'q'. Other styles of application loops are
/// possible, for example, you could have multiple application states and switch between them based /// possible, for example, you could have multiple application states and switch between them based
/// on events, or you could have a single application state and update it based on events. /// on events, or you could have a single application state and update it based on events.
fn run(mut terminal: DefaultTerminal) -> Result<()> { fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
loop { loop {
terminal.draw(draw)?; terminal.draw(crate::render_app)?;
if should_quit()? { if should_quit()? {
break; break;
} }
@@ -50,18 +59,17 @@ fn run(mut terminal: DefaultTerminal) -> Result<()> {
Ok(()) Ok(())
} }
/// Render the application. This is where you would draw the application UI. This example draws a /// Render the application. This is where you would draw the application UI. This example just
/// greeting. /// draws a greeting.
fn draw(frame: &mut Frame) { fn render_app(frame: &mut Frame) {
let greeting = Paragraph::new("Hello World! (press 'q' to quit)"); let greeting = Paragraph::new("Hello World! (press 'q' to quit)");
frame.render_widget(greeting, frame.area()); frame.render_widget(greeting, frame.size());
} }
/// Check if the user has pressed 'q'. This is where you would handle events. This example just /// Check if the user has pressed 'q'. This is where you would handle events. This example just
/// checks if the user has pressed 'q' and returns true if they have. It does not handle any other /// checks if the user has pressed 'q' and returns true if they have. It does not handle any other
/// events. There is a 250ms timeout on the event poll to ensure that the terminal is rendered at /// events. There is a 250ms timeout on the event poll so that the application can exit in a timely
/// least once every 250ms. This allows you to do other work in the application loop, such as /// manner, and to ensure that the terminal is rendered at least once every 250ms.
/// updating the application state, without blocking the event loop for too long.
fn should_quit() -> Result<bool> { fn should_quit() -> Result<bool> {
if event::poll(Duration::from_millis(250)).context("event poll failed")? { if event::poll(Duration::from_millis(250)).context("event poll failed")? {
if let Event::Key(key) = event::read().context("event read failed")? { if let Event::Key(key) = event::read().context("event read failed")? {

View File

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

View File

@@ -1,72 +1,28 @@
//! # [Ratatui] Inline example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use std::{ use std::{
collections::{BTreeMap, VecDeque}, collections::{BTreeMap, VecDeque},
error::Error,
io,
sync::mpsc, sync::mpsc,
thread, thread,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use color_eyre::Result;
use rand::distributions::{Distribution, Uniform}; use rand::distributions::{Distribution, Uniform};
use ratatui::{ use ratatui::{prelude::*, widgets::*};
backend::Backend,
crossterm::event,
layout::{Constraint, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
text::{Line, Span},
widgets::{Block, Gauge, LineGauge, List, ListItem, Paragraph, Widget},
Frame, Terminal, TerminalOptions, Viewport,
};
fn main() -> Result<()> {
color_eyre::install()?;
let mut terminal = ratatui::init_with_options(TerminalOptions {
viewport: Viewport::Inline(8),
});
let (tx, rx) = mpsc::channel();
input_handling(tx.clone());
let workers = workers(tx);
let mut downloads = downloads();
for w in &workers {
let d = downloads.next(w.id).unwrap();
w.tx.send(d).unwrap();
}
let app_result = run(&mut terminal, workers, downloads, rx);
ratatui::restore();
app_result
}
const NUM_DOWNLOADS: usize = 10; const NUM_DOWNLOADS: usize = 10;
type DownloadId = usize; type DownloadId = usize;
type WorkerId = usize; type WorkerId = usize;
enum Event { enum Event {
Input(event::KeyEvent), Input(crossterm::event::KeyEvent),
Tick, Tick,
Resize, Resize,
DownloadUpdate(WorkerId, DownloadId, f64), DownloadUpdate(WorkerId, DownloadId, f64),
DownloadDone(WorkerId, DownloadId), DownloadDone(WorkerId, DownloadId),
} }
struct Downloads { struct Downloads {
pending: VecDeque<Download>, pending: VecDeque<Download>,
in_progress: BTreeMap<WorkerId, DownloadInProgress>, in_progress: BTreeMap<WorkerId, DownloadInProgress>,
@@ -90,20 +46,52 @@ impl Downloads {
} }
} }
} }
struct DownloadInProgress { struct DownloadInProgress {
id: DownloadId, id: DownloadId,
started_at: Instant, started_at: Instant,
progress: f64, progress: f64,
} }
struct Download { struct Download {
id: DownloadId, id: DownloadId,
size: usize, size: usize,
} }
struct Worker { struct Worker {
id: WorkerId, id: WorkerId,
tx: mpsc::Sender<Download>, tx: mpsc::Sender<Download>,
} }
fn main() -> Result<(), Box<dyn Error>> {
crossterm::terminal::enable_raw_mode()?;
let stdout = io::stdout();
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(8),
},
)?;
let (tx, rx) = mpsc::channel();
input_handling(tx.clone());
let workers = workers(tx);
let mut downloads = downloads();
for w in &workers {
let d = downloads.next(w.id).unwrap();
w.tx.send(d).unwrap();
}
run_app(&mut terminal, workers, downloads, rx)?;
crossterm::terminal::disable_raw_mode()?;
terminal.clear()?;
Ok(())
}
fn input_handling(tx: mpsc::Sender<Event>) { fn input_handling(tx: mpsc::Sender<Event>) {
let tick_rate = Duration::from_millis(200); let tick_rate = Duration::from_millis(200);
thread::spawn(move || { thread::spawn(move || {
@@ -111,10 +99,10 @@ fn input_handling(tx: mpsc::Sender<Event>) {
loop { loop {
// poll for tick rate duration, if no events, sent tick event. // poll for tick rate duration, if no events, sent tick event.
let timeout = tick_rate.saturating_sub(last_tick.elapsed()); let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout).unwrap() { if crossterm::event::poll(timeout).unwrap() {
match event::read().unwrap() { match crossterm::event::read().unwrap() {
event::Event::Key(key) => tx.send(Event::Input(key)).unwrap(), crossterm::event::Event::Key(key) => tx.send(Event::Input(key)).unwrap(),
event::Event::Resize(_, _) => tx.send(Event::Resize).unwrap(), crossterm::event::Event::Resize(_, _) => tx.send(Event::Resize).unwrap(),
_ => {} _ => {}
}; };
} }
@@ -126,7 +114,6 @@ fn input_handling(tx: mpsc::Sender<Event>) {
}); });
} }
#[allow(clippy::cast_precision_loss, clippy::needless_pass_by_value)]
fn workers(tx: mpsc::Sender<Event>) -> Vec<Worker> { fn workers(tx: mpsc::Sender<Event>) -> Vec<Worker> {
(0..4) (0..4)
.map(|id| { .map(|id| {
@@ -166,23 +153,22 @@ fn downloads() -> Downloads {
} }
} }
#[allow(clippy::needless_pass_by_value)] fn run_app<B: Backend>(
fn run( terminal: &mut Terminal<B>,
terminal: &mut Terminal<impl Backend>,
workers: Vec<Worker>, workers: Vec<Worker>,
mut downloads: Downloads, mut downloads: Downloads,
rx: mpsc::Receiver<Event>, rx: mpsc::Receiver<Event>,
) -> Result<()> { ) -> Result<(), Box<dyn Error>> {
let mut redraw = true; let mut redraw = true;
loop { loop {
if redraw { if redraw {
terminal.draw(|frame| draw(frame, &downloads))?; terminal.draw(|f| ui(f, &downloads))?;
} }
redraw = true; redraw = true;
match rx.recv()? { match rx.recv()? {
Event::Input(event) => { Event::Input(event) => {
if event.code == event::KeyCode::Char('q') { if event.code == crossterm::event::KeyCode::Char('q') {
break; break;
} }
} }
@@ -193,7 +179,7 @@ fn run(
Event::DownloadUpdate(worker_id, _download_id, progress) => { Event::DownloadUpdate(worker_id, _download_id, progress) => {
let download = downloads.in_progress.get_mut(&worker_id).unwrap(); let download = downloads.in_progress.get_mut(&worker_id).unwrap();
download.progress = progress; download.progress = progress;
redraw = false; redraw = false
} }
Event::DownloadDone(worker_id, download_id) => { Event::DownloadDone(worker_id, download_id) => {
let download = downloads.in_progress.remove(&worker_id).unwrap(); let download = downloads.in_progress.remove(&worker_id).unwrap();
@@ -228,25 +214,24 @@ fn run(
Ok(()) Ok(())
} }
fn draw(frame: &mut Frame, downloads: &Downloads) { fn ui(f: &mut Frame, downloads: &Downloads) {
let area = frame.area(); let area = f.size();
let block = Block::new().title(Line::from("Progress").centered()); let block = Block::default().title(block::Title::from("Progress").alignment(Alignment::Center));
frame.render_widget(block, area); f.render_widget(block, area);
let vertical = Layout::vertical([Constraint::Length(2), Constraint::Length(4)]).margin(1); let vertical = Layout::vertical([Constraint::Length(2), Constraint::Length(4)]).margin(1);
let horizontal = Layout::horizontal([Constraint::Percentage(20), Constraint::Percentage(80)]); let horizontal = Layout::horizontal([Constraint::Percentage(20), Constraint::Percentage(80)]);
let [progress_area, main] = vertical.areas(area); let [progress_area, main] = area.split(&vertical);
let [list_area, gauge_area] = horizontal.areas(main); let [list_area, gauge_area] = main.split(&horizontal);
// total progress // total progress
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len(); let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
#[allow(clippy::cast_precision_loss)]
let progress = LineGauge::default() let progress = LineGauge::default()
.filled_style(Style::default().fg(Color::Blue)) .gauge_style(Style::default().fg(Color::Blue))
.label(format!("{done}/{NUM_DOWNLOADS}")) .label(format!("{done}/{NUM_DOWNLOADS}"))
.ratio(done as f64 / NUM_DOWNLOADS as f64); .ratio(done as f64 / NUM_DOWNLOADS as f64);
frame.render_widget(progress, progress_area); f.render_widget(progress, progress_area);
// in progress downloads // in progress downloads
let items: Vec<ListItem> = downloads let items: Vec<ListItem> = downloads
@@ -269,9 +254,8 @@ fn draw(frame: &mut Frame, downloads: &Downloads) {
}) })
.collect(); .collect();
let list = List::new(items); let list = List::new(items);
frame.render_widget(list, list_area); f.render_widget(list, list_area);
#[allow(clippy::cast_possible_truncation)]
for (i, (_, download)) in downloads.in_progress.iter().enumerate() { for (i, (_, download)) in downloads.in_progress.iter().enumerate() {
let gauge = Gauge::default() let gauge = Gauge::default()
.gauge_style(Style::default().fg(Color::Yellow)) .gauge_style(Style::default().fg(Color::Yellow))
@@ -279,7 +263,7 @@ fn draw(frame: &mut Frame, downloads: &Downloads) {
if gauge_area.top().saturating_add(i as u16) > area.bottom() { if gauge_area.top().saturating_add(i as u16) > area.bottom() {
continue; continue;
} }
frame.render_widget( f.render_widget(
gauge, gauge,
Rect { Rect {
x: gauge_area.left(), x: gauge_area.left(),

View File

@@ -1,63 +1,65 @@
//! # [Ratatui] Layout example use std::{error::Error, io};
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use itertools::Itertools; use crossterm::{
use ratatui::{ event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
crossterm::event::{self, Event, KeyCode, KeyEventKind}, execute,
layout::{ terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
Constraint::{self, Length, Max, Min, Percentage, Ratio},
Layout, Rect,
},
style::{Color, Style, Stylize},
text::Line,
widgets::{Block, Paragraph},
DefaultTerminal, Frame,
}; };
use itertools::Itertools;
use ratatui::{layout::Constraint::*, prelude::*, widgets::*};
fn main() -> color_eyre::Result<()> { fn main() -> Result<(), Box<dyn Error>> {
color_eyre::install()?; // setup terminal
let terminal = ratatui::init(); enable_raw_mode()?;
let app_result = run(terminal); let mut stdout = io::stdout();
ratatui::restore(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
app_result let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let res = run_app(&mut terminal);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
} }
fn run(mut terminal: DefaultTerminal) -> color_eyre::Result<()> { fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
loop { loop {
terminal.draw(draw)?; terminal.draw(ui)?;
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { if let KeyCode::Char('q') = key.code {
break Ok(()); return Ok(());
} }
} }
} }
} }
#[allow(clippy::too_many_lines)] fn ui(frame: &mut Frame) {
fn draw(frame: &mut Frame) {
let vertical = Layout::vertical([ let vertical = Layout::vertical([
Length(4), // text Length(4), // text
Length(50), // examples Length(50), // examples
Min(0), // fills remaining space Min(0), // fills remaining space
]); ]);
let [text_area, examples_area, _] = vertical.areas(frame.area()); let [text_area, examples_area, _] = frame.size().split(&vertical);
// title // title
frame.render_widget( frame.render_widget(
Paragraph::new(vec![ Paragraph::new(vec![
Line::from("Horizontal Layout Example. Press q to quit".dark_gray()).centered(), Line::from("Horizontal Layout Example. Press q to quit".dark_gray())
.alignment(Alignment::Center),
Line::from("Each line has 2 constraints, plus Min(0) to fill the remaining space."), Line::from("Each line has 2 constraints, plus Min(0) to fill the remaining space."),
Line::from("E.g. the second line of the Len/Min box is [Length(2), Min(2), Min(0)]"), Line::from("E.g. the second line of the Len/Min box is [Length(2), Min(2), Min(0)]"),
Line::from("Note: constraint labels that don't fit are truncated"), Line::from("Note: constraint labels that don't fit are truncated"),
@@ -74,28 +76,31 @@ fn draw(frame: &mut Frame) {
Min(0), // fills remaining space Min(0), // fills remaining space
]) ])
.split(examples_area); .split(examples_area);
let example_areas = example_rows.iter().flat_map(|area| { let example_areas = example_rows
Layout::horizontal([
Length(14),
Length(14),
Length(14),
Length(14),
Length(14),
Min(0), // fills remaining space
])
.split(*area)
.iter() .iter()
.copied() .flat_map(|area| {
.take(5) // ignore Min(0) Layout::horizontal([
.collect_vec() Constraint::Length(14),
}); Constraint::Length(14),
Constraint::Length(14),
Constraint::Length(14),
Constraint::Length(14),
Constraint::Min(0), // fills remaining space
])
.split(*area)
.iter()
.copied()
.take(5) // ignore Min(0)
.collect_vec()
})
.collect_vec();
// the examples are a cartesian product of the following constraints // the examples are a cartesian product of the following constraints
// e.g. Len/Len, Len/Min, Len/Max, Len/Perc, Len/Ratio, Min/Len, Min/Min, ... // e.g. Len/Len, Len/Min, Len/Max, Len/Perc, Len/Ratio, Min/Len, Min/Min, ...
let examples = [ let examples = [
( (
"Len", "Len",
[ vec![
Length(0), Length(0),
Length(2), Length(2),
Length(3), Length(3),
@@ -104,11 +109,17 @@ fn draw(frame: &mut Frame) {
Length(15), Length(15),
], ],
), ),
("Min", [Min(0), Min(2), Min(3), Min(6), Min(10), Min(15)]), (
("Max", [Max(0), Max(2), Max(3), Max(6), Max(10), Max(15)]), "Min",
vec![Min(0), Min(2), Min(3), Min(6), Min(10), Min(15)],
),
(
"Max",
vec![Max(0), Max(2), Max(3), Max(6), Max(10), Max(15)],
),
( (
"Perc", "Perc",
[ vec![
Percentage(0), Percentage(0),
Percentage(25), Percentage(25),
Percentage(50), Percentage(50),
@@ -119,7 +130,7 @@ fn draw(frame: &mut Frame) {
), ),
( (
"Ratio", "Ratio",
[ vec![
Ratio(0, 4), Ratio(0, 4),
Ratio(1, 4), Ratio(1, 4),
Ratio(2, 4), Ratio(2, 4),
@@ -130,15 +141,24 @@ fn draw(frame: &mut Frame) {
), ),
]; ];
for ((a, b), area) in examples for (i, (a, b)) in examples
.iter() .iter()
.cartesian_product(examples.iter()) .cartesian_product(examples.iter())
.zip(example_areas) .enumerate()
{ {
let (name_a, examples_a) = a; let (name_a, examples_a) = a;
let (name_b, examples_b) = b; let (name_b, examples_b) = b;
let constraints = examples_a.iter().copied().zip(examples_b.iter().copied()); let constraints = examples_a
render_example_combination(frame, area, &format!("{name_a}/{name_b}"), constraints); .iter()
.copied()
.zip(examples_b.iter().copied())
.collect_vec();
render_example_combination(
frame,
example_areas[i],
&format!("{name_a}/{name_b}"),
constraints,
);
} }
} }
@@ -147,17 +167,18 @@ fn render_example_combination(
frame: &mut Frame, frame: &mut Frame,
area: Rect, area: Rect,
title: &str, title: &str,
constraints: impl ExactSizeIterator<Item = (Constraint, Constraint)>, constraints: Vec<(Constraint, Constraint)>,
) { ) {
let block = Block::bordered() let block = Block::default()
.title(title.gray()) .title(title.gray())
.style(Style::reset()) .style(Style::reset())
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::DarkGray)); .border_style(Style::default().fg(Color::DarkGray));
let inner = block.inner(area); let inner = block.inner(area);
frame.render_widget(block, area); frame.render_widget(block, area);
let layout = Layout::vertical(vec![Length(1); constraints.len() + 1]).split(inner); let layout = Layout::vertical(vec![Length(1); constraints.len() + 1]).split(inner);
for ((a, b), &area) in constraints.into_iter().zip(layout.iter()) { for (i, (a, b)) in constraints.iter().enumerate() {
render_single_example(frame, area, vec![a, b, Min(0)]); render_single_example(frame, layout[i], vec![*a, *b, Min(0)]);
} }
// This is to make it easy to visually see the alignment of the examples // This is to make it easy to visually see the alignment of the examples
// with the constraints. // with the constraints.
@@ -170,7 +191,7 @@ fn render_single_example(frame: &mut Frame, area: Rect, constraints: Vec<Constra
let blue = Paragraph::new(constraint_label(constraints[1])).on_blue(); let blue = Paragraph::new(constraint_label(constraints[1])).on_blue();
let green = Paragraph::new("·".repeat(12)).on_green(); let green = Paragraph::new("·".repeat(12)).on_green();
let horizontal = Layout::horizontal(constraints); let horizontal = Layout::horizontal(constraints);
let [r, b, g] = horizontal.areas(area); let [r, b, g] = area.split(&horizontal);
frame.render_widget(red, r); frame.render_widget(red, r);
frame.render_widget(blue, b); frame.render_widget(blue, b);
frame.render_widget(green, g); frame.render_widget(green, g);
@@ -178,11 +199,12 @@ fn render_single_example(frame: &mut Frame, area: Rect, constraints: Vec<Constra
fn constraint_label(constraint: Constraint) -> String { fn constraint_label(constraint: Constraint) -> String {
match constraint { match constraint {
Constraint::Ratio(a, b) => format!("{a}:{b}"), Length(n) => format!("{n}"),
Constraint::Length(n) Min(n) => format!("{n}"),
| Constraint::Min(n) Max(n) => format!("{n}"),
| Constraint::Max(n) Percentage(n) => format!("{n}"),
| Constraint::Percentage(n) Proportional(n) => format!("{n}"),
| Constraint::Fill(n) => format!("{n}"), Fixed(n) => format!("{n}"),
Ratio(a, b) => format!("{a}:{b}"),
} }
} }

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