Compare commits
128 Commits
feat-filte
...
v0.23.1-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c68ebed4f | ||
|
|
232be80325 | ||
|
|
c95a75c5d5 | ||
|
|
c8ab2d5908 | ||
|
|
572df758ba | ||
|
|
b996102837 | ||
|
|
080a05bbd3 | ||
|
|
343c6cdc47 | ||
|
|
5c785b2270 | ||
|
|
ca9bcd3156 | ||
|
|
82b40be4ab | ||
|
|
e098731d6c | ||
|
|
5f6aa30be5 | ||
|
|
ea70bffe5d | ||
|
|
47ae602df4 | ||
|
|
878b6fc258 | ||
|
|
0696f484e8 | ||
|
|
927a5d8251 | ||
|
|
28e7fd4bc5 | ||
|
|
28c61571e8 | ||
|
|
d0779034e7 | ||
|
|
51fdcbe7e9 | ||
|
|
eda2fb7077 | ||
|
|
3f781cad0a | ||
|
|
fc727df7d2 | ||
|
|
47fe4ad69f | ||
|
|
7a70602ec6 | ||
|
|
14eb6b6979 | ||
|
|
6009844e25 | ||
|
|
8b36683571 | ||
|
|
e9bd736b1a | ||
|
|
a890f2ac00 | ||
|
|
b35f19ec44 | ||
|
|
ad3413eeec | ||
|
|
f0716edbcf | ||
|
|
fc9f637fb0 | ||
|
|
292a11d81e | ||
|
|
ad4d6e7dec | ||
|
|
e4bcf78afa | ||
|
|
d0ee04a69f | ||
|
|
6d6eceeb88 | ||
|
|
0dca6a689a | ||
|
|
a937500ae4 | ||
|
|
80fd77e476 | ||
|
|
98155dce25 | ||
|
|
1ba2246d95 | ||
|
|
57ea871753 | ||
|
|
61533712be | ||
|
|
dc552116cf | ||
|
|
ab5e616635 | ||
|
|
b6b2da5eb7 | ||
|
|
89ef0e29f5 | ||
|
|
4cd843eda9 | ||
|
|
d2429bc3e4 | ||
|
|
b090101b23 | ||
|
|
56455e0fee | ||
|
|
f4ed3b7584 | ||
|
|
c86924b925 | ||
|
|
de25de0a95 | ||
|
|
ea48af1c9a | ||
|
|
418ed20479 | ||
|
|
519509945b | ||
|
|
8c55158822 | ||
|
|
7748720963 | ||
|
|
4d70169bef | ||
|
|
10dbd6f207 | ||
|
|
778c320008 | ||
|
|
268bbed17e | ||
|
|
f63ac72305 | ||
|
|
3293c6b80b | ||
|
|
149d48919d | ||
|
|
8c4a2e0fbf | ||
|
|
664fb4cffd | ||
|
|
6ad4bd4cf2 | ||
|
|
37fa6abe9d | ||
|
|
8b28672131 | ||
|
|
de9f52ff2c | ||
|
|
c8ddc164c7 | ||
|
|
e18393dbc6 | ||
|
|
aad164a531 | ||
|
|
3a37d2f6ed | ||
|
|
8cd3205d70 | ||
|
|
e82521ea79 | ||
|
|
9191ad60fd | ||
|
|
49a82e062f | ||
|
|
181706c564 | ||
|
|
554805d6cb | ||
|
|
1727fa5120 | ||
|
|
440f62ff54 | ||
|
|
6f659cfb07 | ||
|
|
bf4944683d | ||
|
|
7539f775fe | ||
|
|
8db9fb4aeb | ||
|
|
d05ab6fb70 | ||
|
|
2920e045ba | ||
|
|
add578a7d6 | ||
|
|
60a4131384 | ||
|
|
964190a859 | ||
|
|
b9290b35d1 | ||
|
|
daf5890152 | ||
|
|
7e37a96678 | ||
|
|
bcb7417785 | ||
|
|
9c956733f7 | ||
|
|
13fb11a62c | ||
|
|
0fb1ed85c6 | ||
|
|
e2cb11cc30 | ||
|
|
c3f87f245a | ||
|
|
df90982632 | ||
|
|
bb061fdab6 | ||
|
|
1ff85535c8 | ||
|
|
33f3212cbf | ||
|
|
fb6d4b2f51 | ||
|
|
446efae185 | ||
|
|
b347201b9f | ||
|
|
9f1f59a51c | ||
|
|
6f6c355c5c | ||
|
|
60150f6236 | ||
|
|
2889c7d084 | ||
|
|
57678a5fe8 | ||
|
|
ae8ed8867d | ||
|
|
e66d5cdee0 | ||
|
|
804115ac6f | ||
|
|
a1813af297 | ||
|
|
085fde7d4a | ||
|
|
860a40c13a | ||
|
|
0833c9018b | ||
|
|
f7c4b44962 | ||
|
|
56e44a0efa |
11
.editorconfig
Normal file
11
.editorconfig
Normal file
@@ -0,0 +1,11 @@
|
||||
# configuration for https://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*.rs]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -5,4 +5,4 @@
|
||||
# https://git-scm.com/docs/gitignore#_pattern_format
|
||||
|
||||
# Maintainers
|
||||
* @orhun @mindoodoo @sayanarijit @sophacles @joshka
|
||||
* @orhun @mindoodoo @sayanarijit @sophacles @joshka @kdheepak
|
||||
|
||||
73
.github/workflows/cd.yml
vendored
73
.github/workflows/cd.yml
vendored
@@ -1,18 +1,85 @@
|
||||
name: Continuous Deployment
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# At 00:00 on Saturday
|
||||
# https://crontab.guru/#0_0_*_*_6
|
||||
- cron: "0 0 * * 6"
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish on crates.io
|
||||
publish-alpha:
|
||||
name: Create an alpha release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
if: ${{ !startsWith(github.event.ref, 'refs/tags/v') }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Publish
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Calculate the next release
|
||||
run: |
|
||||
suffix="alpha"
|
||||
last_tag="$(git tag --sort=committerdate | tail -1)"
|
||||
if [[ "${last_tag}" = *"-${suffix}"* ]]; then
|
||||
# increment the alpha version
|
||||
# e.g. v0.22.1-alpha.12 -> v0.22.1-alpha.13
|
||||
alpha="${last_tag##*-${suffix}.}"
|
||||
next_alpha="$((alpha + 1))"
|
||||
next_tag="${last_tag/%${alpha}/${next_alpha}}"
|
||||
else
|
||||
# increment the patch and start the alpha version from 0
|
||||
# e.g. v0.22.0 -> v0.22.1-alpha.0
|
||||
patch="${last_tag##*.}"
|
||||
next_patch="$((patch + 1))"
|
||||
next_tag="${last_tag/%${patch}/${next_patch}}-${suffix}.0"
|
||||
fi
|
||||
# update the crate version
|
||||
msg="# crate version"
|
||||
sed -E -i "s/^version = .* ${msg}$/version = \"${next_tag#v}\" ${msg}/" Cargo.toml
|
||||
echo "NEXT_TAG=${next_tag}" >> $GITHUB_ENV
|
||||
echo "Next alpha release: ${next_tag} 🐭"
|
||||
|
||||
- name: Publish on crates.io
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: publish
|
||||
args: --allow-dirty --token ${{ secrets.CARGO_TOKEN }}
|
||||
|
||||
- name: Generate a changelog
|
||||
uses: orhun/git-cliff-action@v2
|
||||
with:
|
||||
config: cliff.toml
|
||||
args: --unreleased --tag ${{ env.NEXT_TAG }} --strip header
|
||||
env:
|
||||
OUTPUT: BODY.md
|
||||
|
||||
- name: Publish on GitHub
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
tag: ${{ env.NEXT_TAG }}
|
||||
prerelease: true
|
||||
bodyFile: BODY.md
|
||||
|
||||
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@v3
|
||||
|
||||
- name: Publish on crates.io
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: publish
|
||||
|
||||
23
.github/workflows/check-pr-title.yml
vendored
Normal file
23
.github/workflows/check-pr-title.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Check Pull Requests
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- synchronize
|
||||
merge_group:
|
||||
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
check-title:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check PR title
|
||||
if: github.event_name == 'pull_request_target'
|
||||
uses: amannn/action-semantic-pull-request@v5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
204
.github/workflows/ci.yml
vendored
204
.github/workflows/ci.yml
vendored
@@ -1,78 +1,30 @@
|
||||
name: Continuous Integration
|
||||
|
||||
on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- feat-wrapping
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- feat-wrapping
|
||||
merge_group:
|
||||
|
||||
# ensure that the workflow is only triggered once per PR, subsequent pushes to the PR will cancel
|
||||
# and restart the workflow. See https://docs.github.com/en/actions/using-jobs/using-concurrency
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
name: CI
|
||||
env:
|
||||
# don't install husky hooks during CI as they are only needed for for pre-push
|
||||
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
|
||||
|
||||
# lint, clippy and coveraget jobs are intentionally early in the workflow to catch simple
|
||||
# formatting, typos, and missing tests as early as possible. This allows us to fix these and
|
||||
# resubmit the PR without having to wait for the comprehensive matrix of tests to complete.
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
toolchain: [ "1.65.0", "stable" ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: lint
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: "Check"
|
||||
run: cargo make check
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
|
||||
|
||||
clippy:
|
||||
name: Clippy
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: "Clippy"
|
||||
run: cargo make clippy
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
|
||||
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
toolchain: [ "1.65.0", "stable" ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: lint
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: "Test"
|
||||
run: cargo make test
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -84,38 +36,122 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: "Check conventional commits"
|
||||
uses: crate-ci/committed@master
|
||||
with:
|
||||
args: "-vv"
|
||||
commits: "HEAD"
|
||||
- name: "Check typos"
|
||||
uses: crate-ci/typos@master
|
||||
- name: "Lint dependencies"
|
||||
uses: EmbarkStudios/cargo-deny-action@v1
|
||||
- name: Install Rust
|
||||
- name: Install Rust nightly
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: rustfmt
|
||||
- name: "Formatting"
|
||||
run: cargo fmt --all --check
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Check formatting
|
||||
run: cargo make lint-format
|
||||
- name: Check documentation
|
||||
run: cargo make lint-docs
|
||||
- name: Check conventional commits
|
||||
uses: crate-ci/committed@master
|
||||
with:
|
||||
args: "-vv"
|
||||
commits: HEAD
|
||||
- name: Check typos
|
||||
uses: crate-ci/typos@master
|
||||
- name: Lint dependencies
|
||||
uses: EmbarkStudios/cargo-deny-action@v1
|
||||
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Run cargo make clippy-all
|
||||
run: cargo make clippy
|
||||
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Rust
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: llvm-tools
|
||||
- name: cargo install cargo-llvm-cov
|
||||
uses: taiki-e/install-action@cargo-llvm-cov
|
||||
- name: cargo llvm-cov
|
||||
run: cargo llvm-cov --all-features --lcov --output-path lcov.info
|
||||
env:
|
||||
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
|
||||
- name: Install cargo-llvm-cov and cargo-make
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-llvm-cov,cargo-make
|
||||
- name: Generate coverage
|
||||
run: cargo make coverage
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
check:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
toolchain: [ "1.67.0", "stable" ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Rust {{ matrix.toolchain }}
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Run cargo make check
|
||||
run: cargo make check
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
test-doc:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Test docs
|
||||
run: cargo make test-doc
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
toolchain: [ "1.67.0", "stable" ]
|
||||
backend: [ crossterm, termion, termwiz ]
|
||||
exclude:
|
||||
# termion is not supported on windows
|
||||
- os: windows-latest
|
||||
backend: termion
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Rust ${{ matrix.toolchain }}}
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Test ${{ matrix.backend }}
|
||||
run: cargo make test-backend ${{ matrix.backend }}
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
@@ -7,3 +7,6 @@ no-inline-html:
|
||||
- summary
|
||||
line-length:
|
||||
line_length: 100
|
||||
|
||||
# to support repeated headers in the changelog
|
||||
no-duplicate-heading: false
|
||||
|
||||
1169
CHANGELOG.md
1169
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
234
CONTRIBUTING.md
234
CONTRIBUTING.md
@@ -1,69 +1,215 @@
|
||||
# Fork Status
|
||||
# Contribution guidelines
|
||||
|
||||
## Pull Requests
|
||||
First off, thank you for considering contributing to Ratatui.
|
||||
|
||||
**All** pull requests opened on the original repository have been imported. We'll be going through any open PRs in a timely manner, starting with the **smallest bug fixes and README updates**. If you have an open PR make sure to let us know about it on our [discord](https://discord.gg/pMCEU9hNEj) as it helps to know you are still active.
|
||||
If your contribution is not straightforward, please first discuss the change you wish to make by
|
||||
creating a new issue before making the change, or starting a discussion on
|
||||
[discord](https://discord.gg/pMCEU9hNEj).
|
||||
|
||||
## Issues
|
||||
## Reporting issues
|
||||
|
||||
We have been unsuccessful in importing all issues opened on the previous repository.
|
||||
For that reason, anyone wanting to **work on or discuss** an issue will have to follow the following workflow :
|
||||
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
|
||||
also check [`tui-rs` issues](https://github.com/fdehau/tui-rs/issues/) and link any related issues
|
||||
found.
|
||||
|
||||
- Recreate the issue
|
||||
- Start by referencing the **original issue**: ```Referencing issue #[<issue number>](<original issue link>)```
|
||||
- Then, paste the original issues **opening** text
|
||||
## Pull requests
|
||||
|
||||
You can then resume the conversation by replying to this new issue you have created.
|
||||
All contributions are obviously welcome. Please include as many details as possible in your PR
|
||||
description to help the reviewer (follow the provided template). Make sure to highlight changes
|
||||
which may need additional attention or you are uncertain about. Any idea with a large scale impact
|
||||
on the crate or its users should ideally be discussed in a "Feature Request" issue beforehand.
|
||||
|
||||
### Closing Issues
|
||||
### Keep PRs small, intentional and focused
|
||||
|
||||
If you close an issue that you have "imported" to this fork, please make sure that you add the issue to the **CLOSED_ISSUES.md**. This will enable us to keep track of which issues have been closed from the original repo, in case we are able to have the original repository transferred.
|
||||
Try to do one pull request per change. The time taken to review a PR grows exponential with the size
|
||||
of the change. Small focused PRs will generally be much more faster to review. PRs that include both
|
||||
refactoring (or reformatting) with actual changes are more difficult to review as every line of the
|
||||
change becomes a place where a bug may have been introduced. Consider splitting refactoring /
|
||||
reformatting changes into a separate PR from those that make a behavioral change, as the tests help
|
||||
guarantee that the behavior is unchanged.
|
||||
|
||||
# Contributing
|
||||
### Code formatting
|
||||
|
||||
Run `cargo make format` before committing to ensure that code is consistently formatted with
|
||||
rustfmt. Configuration is in [rustfmt.toml](./rustfmt.toml).
|
||||
|
||||
### Search `tui-rs` for similar work
|
||||
|
||||
The original fork of Ratatui, [`tui-rs`](https://github.com/fdehau/tui-rs/), has a large amount of
|
||||
history of the project. Please search, read, link, and summarize any relevant
|
||||
[issues](https://github.com/fdehau/tui-rs/issues/),
|
||||
[discussions](https://github.com/fdehau/tui-rs/discussions/) and [pull
|
||||
requests](https://github.com/fdehau/tui-rs/pulls).
|
||||
|
||||
### Use conventional commits
|
||||
|
||||
We use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) and check for them as
|
||||
a lint build step. To help adhere to the format, we recommend to install
|
||||
[Commitizen](https://commitizen-tools.github.io/commitizen/). By using this tool you automatically
|
||||
follow the configuration defined in [.cz.toml](.cz.toml). Your commit messages should have enough
|
||||
information to help someone reading the [CHANGELOG](./CHANGELOG.md) understand what is new just from
|
||||
the title. The summary helps expand on that to provide information that helps provide more context,
|
||||
describes the nature of the problem that the commit is solving and any unintuitive effects of the
|
||||
change. It's rare that code changes can easily communicate intent, so make sure this is clearly
|
||||
documented.
|
||||
|
||||
### 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
|
||||
|
||||
We're using [cargo-husky](https://github.com/rhysd/cargo-husky) to automatically run git hooks,
|
||||
which will run `cargo make ci` before each push. To initialize the hook run `cargo test`. If
|
||||
`cargo-make` is not installed, it will provide instructions to install it for you. This will ensure
|
||||
that your code is formatted, compiles and passes all tests before you push. If you need to skip this
|
||||
check, you can use `git push --no-verify`.
|
||||
|
||||
### Sign your commits
|
||||
|
||||
We use commit signature verification, which will block commits from being merged via the UI unless
|
||||
they are signed. To set up your machine to sign commits, see [managing commit signature
|
||||
verification](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification)
|
||||
in GitHub docs.
|
||||
|
||||
## Implementation Guidelines
|
||||
|
||||
### Setup
|
||||
|
||||
Clone the repo and build it using [cargo-make](https://sagiegurari.github.io/cargo-make/)
|
||||
|
||||
Ratatui is an ordinary Rust project where common tasks are managed with
|
||||
[cargo-make](https://github.com/sagiegurari/cargo-make/). It wraps common `cargo` commands with sane
|
||||
defaults depending on your platform of choice. Building the project should be as easy as running
|
||||
`cargo make build`.
|
||||
|
||||
```shell
|
||||
git clone https://github.com/ratatui-org/ratatui.git
|
||||
cd ratatui
|
||||
cargo make build
|
||||
```
|
||||
|
||||
### Tests
|
||||
|
||||
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
|
||||
tests for all new or modified code. Beside the usual doc and unit tests, one of the most valuable
|
||||
test you can write for Ratatui is a test against the `TestBackend`. It allows you to assert the
|
||||
content of the output buffer that would have been flushed to the terminal after a given draw call.
|
||||
See `widgets_block_renders` in [tests/widgets_block.rs](./tests/widget_block.rs) for an example.
|
||||
|
||||
When writing tests, generally prefer to write unit tests and doc tests directly in the code file
|
||||
being tested rather than integration tests in the `tests/` folder.
|
||||
|
||||
If an area that you're making a change in is not tested, write tests to characterize the existing
|
||||
behavior before changing it. This helps ensure that we don't introduce bugs to existing software
|
||||
using Ratatui (and helps make it easy to migrate apps still using `tui-rs`).
|
||||
|
||||
For coverage, we have two [bacon](https://dystroy.org/bacon/) jobs (one for all tests, and one for
|
||||
unit tests, keyboard shortcuts `v` and `u` respectively) that run
|
||||
[cargo-llvm-cov](https://github.com/taiki-e/cargo-llvm-cov) to report the coverage. Several plugins
|
||||
exist to show coverage directly in your editor. E.g.:
|
||||
|
||||
- <https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters>
|
||||
- <https://github.com/alepez/vim-llvmcov>
|
||||
|
||||
### Documentation
|
||||
|
||||
Here are some guidelines for writing documentation in Ratatui.
|
||||
Every public API **must** be documented.
|
||||
|
||||
Keep in mind that Ratatui tends to attract beginner Rust users that may not be familiar with Rust
|
||||
concepts.
|
||||
|
||||
#### Content
|
||||
|
||||
The main doc comment should talk about the general features that the widget supports and introduce
|
||||
the concepts pointing to the various methods. Focus on interaction with various features and giving
|
||||
enough information that helps understand why you might want something.
|
||||
|
||||
Examples should help users understand a particular usage, not test a feature. They should be as
|
||||
simple as possible.
|
||||
Prefer hiding imports and using wildcards to keep things concise. Some imports may still be shown
|
||||
to demonstrate a particular non-obvious import (e.g. `Stylize` trait to use style methods).
|
||||
Speaking of `Stylize`, you should use it over the more verbose style setters:
|
||||
|
||||
```rust
|
||||
let style = Style::new().red().bold();
|
||||
// not
|
||||
let style = Style::default().fg(Color::Red).add_modifier(Modifiers::BOLD);
|
||||
```
|
||||
|
||||
#### Format
|
||||
|
||||
- First line is summary, second is blank, third onward is more detail
|
||||
|
||||
```rust
|
||||
/// Summary
|
||||
///
|
||||
/// A detailed description
|
||||
/// with examples.
|
||||
fn foo() {}
|
||||
```
|
||||
|
||||
- Max line length is 100 characters
|
||||
See [vscode rewrap extension](https://marketplace.visualstudio.com/items?itemName=stkb.rewrap)
|
||||
|
||||
- Doc comments are above macros
|
||||
i.e.
|
||||
|
||||
```rust
|
||||
/// doc comment
|
||||
#[derive(Debug)]
|
||||
struct Foo {}
|
||||
```
|
||||
|
||||
- Code items should be between backticks
|
||||
i.e. ``[`Block`]``, **NOT** ``[Block]``
|
||||
|
||||
### Use of unsafe for optimization purposes
|
||||
|
||||
**Do not** use unsafe to achieve better performances. This is subject to change, [see.](https://github.com/tui-rs-revival/tui-rs-revival/discussions/66)
|
||||
The only exception to this rule is if it's to fix **reproducible slowness.**
|
||||
|
||||
## Building
|
||||
|
||||
[cargo-make]: https://github.com/sagiegurari/cargo-make "cargo-make"
|
||||
|
||||
`ratatui` is an ordinary Rust project where common tasks are managed with [cargo-make].
|
||||
It wraps common `cargo` commands with sane defaults depending on your platform of choice.
|
||||
Building the project should be as easy as running `cargo make build`.
|
||||
|
||||
## :hammer_and_wrench: Pull requests
|
||||
|
||||
All contributions are obviously welcome.
|
||||
Please include as many details as possible in your PR description to help the reviewer (follow the provided template).
|
||||
Make sure to highlight changes which may need additional attention or you are uncertain about.
|
||||
Any idea with a large scale impact on the crate or its users should ideally be discussed in a "Feature Request" issue beforehand.
|
||||
|
||||
## Committing
|
||||
|
||||
To avoid any issues that may arrise with the CI/CD by not following the [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) syntax, we recommend to install [Commitizen](https://commitizen-tools.github.io/commitizen/).\
|
||||
By using this tool you automatically follow the configuration defined in [.cz.toml](.cz.toml).
|
||||
|
||||
Additionally, we're using [cargo-husky](https://github.com/rhysd/cargo-husky) to automatically load pre-push hook, which will run `cargo make ci` before each push. It will load the hook automatically when you run `cargo test`. If `cargo-make` is not installed, it will install it for you.\
|
||||
This will ensure that your code is formatted, compiles and passes all tests before you push. If you want to skip this check, you can use `git push --no-verify`.
|
||||
We don't currently use any unsafe code in Ratatui, and would like to keep it that way. However there
|
||||
may be specific cases that this becomes necessary in order to avoid slowness. Please see [this
|
||||
discussion](https://github.com/ratatui-org/ratatui/discussions/66) for more about the decision.
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
We use Github Actions for the CI where we perform the following checks:
|
||||
|
||||
- The code should compile on `stable` and the Minimum Supported Rust Version (MSRV).
|
||||
- The tests (docs, lib, tests and examples) should pass.
|
||||
- The code should conform to the default format enforced by `rustfmt`.
|
||||
- The code should not contain common style issues `clippy`.
|
||||
|
||||
You can also check most of those things yourself locally using `cargo make ci` which will offer you a shorter feedback loop.
|
||||
You can also check most of those things yourself locally using `cargo make ci` which will offer you
|
||||
a shorter feedback loop than pushing to github.
|
||||
|
||||
## Tests
|
||||
## Relationship with `tui-rs`
|
||||
|
||||
The test coverage of the crate is far from being ideal but we already have a fair amount of tests in place.
|
||||
Beside the usual doc and unit tests, one of the most valuable test you can write for `ratatui` is a test against the `TestBackend`.
|
||||
It allows you to assert the content of the output buffer that would have been flushed to the terminal after a given draw call.
|
||||
See `widgets_block_renders` in [tests/widgets_block.rs](./tests/widget_block.rs) for an example.
|
||||
This project was forked from [`tui-rs`](https://github.com/fdehau/tui-rs/) in February 2023, with the
|
||||
[blessing of the original author](https://github.com/fdehau/tui-rs/issues/654), Florian Dehau
|
||||
([@fdehau](https://github.com/fdehau)).
|
||||
|
||||
The original repository contains all the issues, PRs and discussion that were raised originally, and
|
||||
it is useful to refer to when contributing code, documentation, or issues with Ratatui.
|
||||
|
||||
We imported all the PRs from the original repository and implemented many of the smaller ones and
|
||||
made notes on the leftovers. These are marked as draft PRs and labelled as [imported from
|
||||
tui](https://github.com/ratatui-org/ratatui/pulls?q=is%3Apr+is%3Aopen+label%3A%22imported+from+tui%22).
|
||||
We have documented the current state of those PRs, and anyone is welcome to pick them up and
|
||||
continue the work on them.
|
||||
|
||||
We have not imported all issues opened on the previous repository. For that reason, anyone wanting
|
||||
to **work on or discuss** an issue will have to follow the following workflow:
|
||||
|
||||
- Recreate the issue
|
||||
- Start by referencing the **original issue**: ```Referencing issue #[<issue number>](<original
|
||||
issue link>)```
|
||||
- Then, paste the original issues **opening** text
|
||||
|
||||
You can then resume the conversation by replying to this new issue you have created.
|
||||
|
||||
106
Cargo.toml
106
Cargo.toml
@@ -1,11 +1,11 @@
|
||||
[package]
|
||||
name = "ratatui"
|
||||
version = "0.21.0"
|
||||
authors = ["Florian Dehau <work@fdehau.com>"]
|
||||
description = "A library to build rich terminal user interfaces or dashboards"
|
||||
version = "0.23.0" # crate version
|
||||
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
|
||||
description = "A library that's all about cooking up terminal user interfaces"
|
||||
documentation = "https://docs.rs/ratatui/latest/ratatui/"
|
||||
keywords = ["tui", "terminal", "dashboard"]
|
||||
repository = "https://github.com/tui-rs-revival/ratatui"
|
||||
repository = "https://github.com/ratatui-org/ratatui"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
exclude = [
|
||||
@@ -18,47 +18,89 @@ exclude = [
|
||||
]
|
||||
autoexamples = true
|
||||
edition = "2021"
|
||||
rust-version = "1.65.0"
|
||||
rust-version = "1.67.0"
|
||||
|
||||
[badges]
|
||||
|
||||
[dependencies]
|
||||
#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.
|
||||
#!
|
||||
#! Generally an application will only use one backend, so you should only enable one of the following features:
|
||||
## enables the [`CrosstermBackend`] backend and adds a dependency on the [Crossterm crate].
|
||||
crossterm = { version = "0.27", optional = true }
|
||||
## enables the [`TermionBackend`] backend and adds a dependency on the [Termion crate].
|
||||
termion = { version = "2.0", optional = true }
|
||||
## enables the [`TermwizBackend`] backend and adds a dependency on the [Termwiz crate].
|
||||
termwiz = { version = "0.20.0", optional = true }
|
||||
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
bitflags = "2.3"
|
||||
cassowary = "0.3"
|
||||
indoc = "2.0"
|
||||
itertools = "0.11"
|
||||
paste = "1.0.2"
|
||||
strum = { version = "0.25", features = ["derive"] }
|
||||
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
|
||||
unicode-segmentation = "1.10"
|
||||
unicode-width = "0.1"
|
||||
document-features = { version = "0.2.7", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.71"
|
||||
argh = "0.1"
|
||||
cargo-husky = { version = "1.5.0", default-features = false, features = [
|
||||
"user-hooks",
|
||||
] }
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
fakeit = "1.1"
|
||||
rand = "0.8"
|
||||
pretty_assertions = "1.4.0"
|
||||
|
||||
[features]
|
||||
default = ["crossterm"]
|
||||
all-widgets = ["widget-calendar"]
|
||||
widget-calendar = ["time"]
|
||||
macros = []
|
||||
#! The following optional features are available for all backends:
|
||||
## enables serialization and deserialization of style and color types using the [Serde crate].
|
||||
## This is useful if you want to save themes to a file.
|
||||
serde = ["dep:serde", "bitflags/serde"]
|
||||
|
||||
## enables the [`border!`] macro.
|
||||
macros = []
|
||||
|
||||
## enables all widgets.
|
||||
all-widgets = ["widget-calendar"]
|
||||
|
||||
#! Widgets that add dependencies are gated behind feature flags to prevent unused transitive
|
||||
#! dependencies. The available features are:
|
||||
## enables the [`calendar`] widget module and adds a dependency on the [Time crate].
|
||||
widget-calendar = ["dep:time"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
# see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html
|
||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[dependencies]
|
||||
bitflags = "2.3"
|
||||
cassowary = "0.3"
|
||||
crossterm = { version = "0.26", optional = true }
|
||||
indoc = "2.0"
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
termion = { version = "2.0", optional = true }
|
||||
termwiz = { version = "0.20.0", optional = true }
|
||||
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
|
||||
unicode-segmentation = "1.10"
|
||||
unicode-width = "0.1"
|
||||
[[bench]]
|
||||
name = "barchart"
|
||||
harness = false
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.71"
|
||||
argh = "0.1"
|
||||
cargo-husky = { version = "1.5.0", default-features = false, features = ["user-hooks"] }
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
fakeit = "1.1"
|
||||
rand = "0.8"
|
||||
[[bench]]
|
||||
name = "block"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "list"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "paragraph"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "sparkline"
|
||||
harness = false
|
||||
|
||||
|
||||
[[example]]
|
||||
name = "barchart"
|
||||
required-features = ["crossterm"]
|
||||
@@ -84,6 +126,12 @@ name = "chart"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "colors"
|
||||
required-features = ["crossterm"]
|
||||
# this example is a bit verbose, so we don't want to include it in the docs
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "custom_widget"
|
||||
required-features = ["crossterm"]
|
||||
@@ -114,6 +162,12 @@ name = "list"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "modifiers"
|
||||
required-features = ["crossterm"]
|
||||
# this example is a bit verbose, so we don't want to include it in the docs
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "panic"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
3
LICENSE
3
LICENSE
@@ -1,6 +1,7 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Florian Dehau
|
||||
Copyright (c) 2016-2022 Florian Dehau
|
||||
Copyright (c) 2023 The Ratatui Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
298
Makefile.toml
298
Makefile.toml
@@ -3,265 +3,153 @@
|
||||
[config]
|
||||
skip_core_tasks = true
|
||||
|
||||
[env]
|
||||
# all features except the backend ones
|
||||
ALL_FEATURES = "all-widgets,macros,serde"
|
||||
|
||||
# Windows does not support building termion, so this avoids the build failure by providing two
|
||||
# sets of flags, one for Windows and one for other platforms.
|
||||
# Windows: --features=all-widgets,macros,serde,crossterm,termwiz
|
||||
# Other: --all-features
|
||||
ALL_FEATURES_FLAG = { source = "${CARGO_MAKE_RUST_TARGET_OS}", default_value = "--all-features", mapping = { "windows" = "--features=all-widgets,macros,serde,crossterm,termwiz" } }
|
||||
|
||||
[tasks.default]
|
||||
alias = "ci"
|
||||
|
||||
[tasks.ci]
|
||||
run_task = [
|
||||
{ name = "ci-unix", condition = { platforms = [
|
||||
"linux",
|
||||
"mac",
|
||||
] } },
|
||||
{ name = "ci-windows", condition = { platforms = [
|
||||
"windows",
|
||||
] } },
|
||||
]
|
||||
description = "Run continuous integration tasks"
|
||||
dependencies = ["lint-style", "clippy", "check", "test"]
|
||||
|
||||
[tasks.ci-unix]
|
||||
private = true
|
||||
dependencies = [
|
||||
"style-check",
|
||||
"check-unix",
|
||||
"test-unix",
|
||||
"clippy-unix",
|
||||
]
|
||||
[tasks.lint-style]
|
||||
description = "Lint code style (formatting, typos, docs)"
|
||||
dependencies = ["lint-format", "lint-typos", "lint-docs"]
|
||||
|
||||
[tasks.ci-windows]
|
||||
private = true
|
||||
dependencies = [
|
||||
"style-check",
|
||||
"check-windows",
|
||||
"test-windows",
|
||||
"clippy-windows",
|
||||
]
|
||||
|
||||
[tasks.style-check]
|
||||
dependencies = ["fmt", "typos"]
|
||||
|
||||
[tasks.fmt]
|
||||
[tasks.lint-format]
|
||||
description = "Lint code formatting"
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = ["fmt", "--all", "--check"]
|
||||
|
||||
[tasks.typos]
|
||||
[tasks.format]
|
||||
description = "Fix code formatting"
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = ["fmt", "--all"]
|
||||
|
||||
[tasks.lint-typos]
|
||||
description = "Run typo checks"
|
||||
install_crate = { crate_name = "typos-cli", binary = "typos", test_arg = "--version" }
|
||||
command = "typos"
|
||||
|
||||
[tasks.check]
|
||||
run_task = [
|
||||
{ name = "check-unix", condition = { platforms = [
|
||||
"linux",
|
||||
"mac",
|
||||
] } },
|
||||
{ name = "check-windows", condition = { platforms = [
|
||||
"windows",
|
||||
] } },
|
||||
]
|
||||
|
||||
[tasks.check-unix]
|
||||
private = true
|
||||
dependencies = [
|
||||
"check-crossterm",
|
||||
"check-termion",
|
||||
"check-termwiz",
|
||||
]
|
||||
|
||||
[tasks.check-windows]
|
||||
private = true
|
||||
dependencies = [
|
||||
"check-crossterm",
|
||||
"check-termwiz",
|
||||
]
|
||||
|
||||
[tasks.check-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm" }
|
||||
run_task = "check-backend"
|
||||
|
||||
[tasks.check-termion]
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "check-backend"
|
||||
|
||||
[tasks.check-termwiz]
|
||||
env = { TUI_FEATURES = "serde,termwiz" }
|
||||
run_task = "check-backend"
|
||||
|
||||
[tasks.check-backend]
|
||||
[tasks.lint-docs]
|
||||
description = "Check documentation for errors and warnings"
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = [
|
||||
"rustdoc",
|
||||
"--no-default-features",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
"--",
|
||||
"-Zunstable-options",
|
||||
"--check",
|
||||
"-Dwarnings",
|
||||
]
|
||||
|
||||
[tasks.check]
|
||||
description = "Check code for errors and warnings"
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
args = [
|
||||
"check",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
run_task = [
|
||||
{ name = "build-unix", condition = { platforms = [
|
||||
"linux",
|
||||
"mac",
|
||||
] } },
|
||||
{ name = "build-windows", condition = { platforms = [
|
||||
"windows",
|
||||
] } },
|
||||
]
|
||||
|
||||
[tasks.build-unix]
|
||||
private = true
|
||||
dependencies = [
|
||||
"build-crossterm",
|
||||
"build-termion",
|
||||
"build-termwiz",
|
||||
]
|
||||
|
||||
[tasks.build-windows]
|
||||
private = true
|
||||
dependencies = [
|
||||
"build-crossterm",
|
||||
"build-termwiz",
|
||||
]
|
||||
|
||||
[tasks.build-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm" }
|
||||
run_task = "build-backend"
|
||||
|
||||
[tasks.build-termion]
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "build-backend"
|
||||
|
||||
[tasks.build-termwiz]
|
||||
env = { TUI_FEATURES = "serde,termwiz" }
|
||||
run_task = "build-backend"
|
||||
|
||||
[tasks.build-backend]
|
||||
description = "Compile the project"
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
args = [
|
||||
"build",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
]
|
||||
|
||||
[tasks.clippy]
|
||||
run_task = [
|
||||
{ name = "clippy-unix", condition = { platforms = [
|
||||
"linux",
|
||||
"mac",
|
||||
] } },
|
||||
{ name = "clippy-windows", condition = { platforms = [
|
||||
"windows",
|
||||
] } },
|
||||
]
|
||||
|
||||
[tasks.clippy-unix]
|
||||
private = true
|
||||
dependencies = [
|
||||
"clippy-crossterm",
|
||||
"clippy-termion",
|
||||
"clippy-termwiz",
|
||||
]
|
||||
|
||||
[tasks.clippy-windows]
|
||||
private = true
|
||||
dependencies = [
|
||||
"clippy-crossterm",
|
||||
"clippy-termwiz",
|
||||
]
|
||||
|
||||
[tasks.clippy-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm" }
|
||||
run_task = "clippy-backend"
|
||||
|
||||
[tasks.clippy-termion]
|
||||
env = { TUI_FEATURES = "serde,termion" }
|
||||
run_task = "clippy-backend"
|
||||
|
||||
[tasks.clippy-termwiz]
|
||||
env = { TUI_FEATURES = "serde,termwiz" }
|
||||
run_task = "clippy-backend"
|
||||
|
||||
[tasks.clippy-backend]
|
||||
description = "Run Clippy for linting"
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
args = [
|
||||
"clippy",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"--tests",
|
||||
"--benches",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--no-default-features",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
"--",
|
||||
"-D",
|
||||
"warnings",
|
||||
]
|
||||
|
||||
[tasks.test]
|
||||
run_task = [
|
||||
{ name = "test-unix", condition = { platforms = [
|
||||
"linux",
|
||||
"mac",
|
||||
] } },
|
||||
{ name = "test-windows", condition = { platforms = [
|
||||
"windows",
|
||||
] } },
|
||||
]
|
||||
|
||||
[tasks.test-unix]
|
||||
private = true
|
||||
dependencies = [
|
||||
"test-crossterm",
|
||||
"test-termion",
|
||||
"test-termwiz",
|
||||
"test-doc",
|
||||
]
|
||||
|
||||
[tasks.test-windows]
|
||||
private = true
|
||||
dependencies = [
|
||||
"test-crossterm",
|
||||
"test-termwiz",
|
||||
"test-doc",
|
||||
]
|
||||
|
||||
[tasks.test-crossterm]
|
||||
env = { TUI_FEATURES = "serde,crossterm,all-widgets,macros" }
|
||||
run_task = "test-backend"
|
||||
|
||||
[tasks.test-termion]
|
||||
env = { TUI_FEATURES = "serde,termion,all-widgets,macros" }
|
||||
run_task = "test-backend"
|
||||
|
||||
[tasks.test-termwiz]
|
||||
env = { TUI_FEATURES = "serde,termwiz,all-widgets,macros" }
|
||||
run_task = "test-backend"
|
||||
|
||||
[tasks.test-backend]
|
||||
description = "Run tests"
|
||||
dependencies = ["test-doc"]
|
||||
command = "cargo"
|
||||
condition = { env_set = ["TUI_FEATURES"] }
|
||||
args = [
|
||||
"test",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${TUI_FEATURES}",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
]
|
||||
|
||||
[tasks.test-doc]
|
||||
description = "Run documentation tests"
|
||||
command = "cargo"
|
||||
args = ["test", "--doc"]
|
||||
args = ["test", "--doc", "--no-default-features", "${ALL_FEATURES_FLAG}"]
|
||||
|
||||
[tasks.test-backend]
|
||||
# takes a command line parameter to specify the backend to test (e.g. "crossterm")
|
||||
description = "Run backend-specific tests"
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${ALL_FEATURES},${@}",
|
||||
]
|
||||
|
||||
[tasks.coverage]
|
||||
description = "Generate code coverage report"
|
||||
command = "cargo"
|
||||
args = [
|
||||
"llvm-cov",
|
||||
"--lcov",
|
||||
"--output-path",
|
||||
"target/lcov.info",
|
||||
"--no-default-features",
|
||||
"${ALL_FEATURES_FLAG}",
|
||||
]
|
||||
|
||||
[tasks.run-example]
|
||||
private = true
|
||||
condition = { env_set = ["TUI_EXAMPLE_NAME"] }
|
||||
command = "cargo"
|
||||
args = ["run", "--release", "--example", "${TUI_EXAMPLE_NAME}"]
|
||||
args = [
|
||||
"run",
|
||||
"--release",
|
||||
"--example",
|
||||
"${TUI_EXAMPLE_NAME}",
|
||||
"--features",
|
||||
"all-widgets",
|
||||
]
|
||||
|
||||
[tasks.build-examples]
|
||||
description = "Compile project examples"
|
||||
command = "cargo"
|
||||
args = ["build", "--examples", "--release"]
|
||||
args = ["build", "--examples", "--release", "--features", "all-widgets"]
|
||||
|
||||
[tasks.run-examples]
|
||||
description = "Run project examples"
|
||||
dependencies = ["build-examples"]
|
||||
script = '''
|
||||
#!@duckscript
|
||||
|
||||
70
README.md
70
README.md
@@ -2,21 +2,22 @@
|
||||
|
||||
<img align="left" src="https://avatars.githubusercontent.com/u/125200832?s=128&v=4">
|
||||
|
||||
`ratatui` is a [Rust](https://www.rust-lang.org) library to build rich terminal user interfaces and
|
||||
dashboards. It is a community fork of the original [tui-rs](https://github.com/fdehau/tui-rs)
|
||||
`ratatui` is a [Rust](https://www.rust-lang.org) library that is all about cooking up terminal user interfaces.
|
||||
It is a community fork of the original [tui-rs](https://github.com/fdehau/tui-rs)
|
||||
project.
|
||||
|
||||
[](https://crates.io/crates/ratatui)
|
||||
[](./LICENSE) [](https://github.com/tui-rs-revival/ratatui/actions?query=workflow%3ACI+)
|
||||
[](https://docs.rs/crate/ratatui/)
|
||||
[](https://crates.io/crates/ratatui)
|
||||
[](./LICENSE) [](https://github.com/ratatui-org/ratatui/actions?query=workflow%3ACI+)
|
||||
[](https://docs.rs/crate/ratatui/)
|
||||
[](https://deps.rs/repo/github/tui-rs-revival/ratatui)
|
||||
[](https://app.codecov.io/gh/tui-rs-revival/ratatui)
|
||||
[](https://discord.gg/pMCEU9hNEj)
|
||||
Status](https://deps.rs/repo/github/ratatui-org/ratatui/status.svg?style=flat-square)](https://deps.rs/repo/github/ratatui-org/ratatui)
|
||||
[](https://app.codecov.io/gh/ratatui-org/ratatui)
|
||||
[](https://discord.gg/pMCEU9hNEj)
|
||||
[](https://matrix.to/#/#ratatui:matrix.org)
|
||||
|
||||
<!-- See RELEASE.md for instructions on creating the demo gif --->
|
||||

|
||||

|
||||
|
||||
<details>
|
||||
<summary>Table of Contents</summary>
|
||||
@@ -51,16 +52,7 @@ Or modify your `Cargo.toml`
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
ratatui = { version = "0.21.0", features = ["all-widgets"]}
|
||||
```
|
||||
|
||||
Ratatui is mostly backwards compatible with `tui-rs`. To migrate an existing project, it may be
|
||||
easier to rename the ratatui dependency to `tui` rather than updating every usage of the crate.
|
||||
E.g.:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
tui = { package = "ratatui", version = "0.21.0", features = ["all-widgets"]}
|
||||
ratatui = { version = "0.23.0", features = ["all-widgets"]}
|
||||
```
|
||||
|
||||
## Introduction
|
||||
@@ -90,7 +82,7 @@ The following example demonstrates the minimal amount of code necessary to setup
|
||||
render "Hello World!". The full code for this example which contains a little more detail is in
|
||||
[hello_world.rs](./examples/hello_world.rs). For more guidance on how to create Ratatui apps, see
|
||||
the [Docs](https://docs.rs/ratatui) and [Examples](#examples). There is also a starter template
|
||||
available at [rust-tui-template](https://github.com/tui-rs-revival/rust-tui-template).
|
||||
available at [rust-tui-template](https://github.com/ratatui-org/rust-tui-template).
|
||||
|
||||
```rust
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
@@ -140,17 +132,19 @@ the community forked the project and created this crate. We look forward to cont
|
||||
started by Florian 🚀
|
||||
|
||||
In order to organize ourselves, we currently use a [Discord server](https://discord.gg/pMCEU9hNEj),
|
||||
feel free to join and come chat! There are also plans to implement a [Matrix](https://matrix.org/)
|
||||
bridge in the near future. **Discord is not a MUST to contribute**. We follow a pretty standard
|
||||
github centered open source workflow keeping the most important conversations on GitHub, open an
|
||||
issue or PR and it will be addressed. 😄
|
||||
feel free to join and come chat! There is also a [Matrix](https://matrix.org/) bridge available at
|
||||
[#ratatui:matrix.org](https://matrix.to/#/#ratatui:matrix.org).
|
||||
|
||||
While we do utilize Discord for coordinating, it's not essential for contributing.
|
||||
Our primary open-source workflow is centered around GitHub.
|
||||
For significant discussions, we rely on GitHub — please open an issue, a discussion or a PR.
|
||||
|
||||
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.
|
||||
|
||||
## Rust version requirements
|
||||
|
||||
Since version 0.21.0, The Minimum Supported Rust Version (MSRV) of `ratatui` is 1.65.0.
|
||||
Since version 0.23.0, The Minimum Supported Rust Version (MSRV) of `ratatui` is 1.67.0.
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -169,7 +163,7 @@ cargo run --example demo --no-default-features --features=termion
|
||||
cargo run --example demo --no-default-features --features=termwiz
|
||||
```
|
||||
|
||||
The UI code for the is in [examples/demo/ui.rs](./examples/demo/ui.rs) while the application state
|
||||
The UI code for this is in [examples/demo/ui.rs](./examples/demo/ui.rs) while the application state
|
||||
is in [examples/demo/app.rs](./examples/demo/app.rs).
|
||||
|
||||
If the user interface contains glyphs that are not displayed correctly by your terminal, you may
|
||||
@@ -188,21 +182,23 @@ More examples are available in the [examples](./examples/) folder.
|
||||
The library comes with the following
|
||||
[widgets](https://docs.rs/ratatui/latest/ratatui/widgets/index.html):
|
||||
|
||||
* [BarChart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
|
||||
* [Block](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html)
|
||||
* [Calendar](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
|
||||
* [Canvas](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/struct.Canvas.html) which allows
|
||||
rendering [points, lines, shapes and a world
|
||||
map](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html)
|
||||
* [BarChart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
|
||||
* [Block](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Block.html)
|
||||
* [Calendar](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
|
||||
* [Chart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Chart.html)
|
||||
* [Clear](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Clear.html)
|
||||
* [Gauge](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Gauge.html)
|
||||
* [List](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html)
|
||||
* [Paragraph](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Paragraph.html)
|
||||
* [Scrollbar](https://docs.rs/ratatui/latest/ratatui/widgets/scrollbar/struct.Scrollbar.html)
|
||||
* [Sparkline](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Sparkline.html)
|
||||
* [Table](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Table.html)
|
||||
* [Tabs](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Tabs.html)
|
||||
|
||||
Each wiget has an associated example which can be found in the [examples](./examples/) folder. Run
|
||||
Each widget has an associated example which can be found in the [examples](./examples/) folder. Run
|
||||
each examples with cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by
|
||||
pressing `q`.
|
||||
|
||||
@@ -212,10 +208,10 @@ be installed with `cargo install cargo-make`).
|
||||
### Third-party libraries, bootstrapping templates and widgets
|
||||
|
||||
* [ansi-to-tui](https://github.com/uttarayan21/ansi-to-tui) — Convert ansi colored text to
|
||||
`tui::text::Text`
|
||||
`ratatui::text::Text`
|
||||
* [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
|
||||
`tui::style::Color`
|
||||
* [rust-tui-template](https://github.com/orhun/rust-tui-template) — A template for bootstrapping a
|
||||
`ratatui::style::Color`
|
||||
* [rust-tui-template](https://github.com/ratatui-org/rust-tui-template) — A template for bootstrapping a
|
||||
Rust TUI application with Tui-rs & crossterm
|
||||
* [simple-tui-rs](https://github.com/pmsanford/simple-tui-rs) — A simple example tui-rs app
|
||||
* [tui-builder](https://github.com/jkelleyrtp/tui-builder) — Batteries-included MVC framework for
|
||||
@@ -241,7 +237,7 @@ be installed with `cargo install cargo-make`).
|
||||
## Apps
|
||||
|
||||
Check out the list of more than 50 [Apps using
|
||||
`Ratatui`](https://github.com/tui-rs-revival/ratatui/wiki/Apps-using-Ratatui)!
|
||||
`Ratatui`](https://github.com/ratatui-org/ratatui/wiki/Apps-using-Ratatui)!
|
||||
|
||||
## Alternatives
|
||||
|
||||
@@ -251,12 +247,12 @@ to build text user interfaces in Rust.
|
||||
## Contributors
|
||||
|
||||
[](https://github.com/tui-rs-revival/ratatui/graphs/contributors)
|
||||
Contributors](https://contrib.rocks/image?repo=ratatui-org/ratatui)](https://github.com/ratatui-org/ratatui/graphs/contributors)
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Special thanks to [**Pavel Fomchenkov**](https://github.com/nawok) for his work in designing **an
|
||||
awesome logo** for the ratatui project and tui-rs-revival organization.
|
||||
awesome logo** for the ratatui project and ratatui-org organization.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
19
RELEASE.md
19
RELEASE.md
@@ -3,28 +3,23 @@
|
||||
[crates.io](https://crates.io/crates/ratatui) releases are automated via [GitHub
|
||||
actions](.github/workflows/cd.yml) and triggered by pushing a tag.
|
||||
|
||||
1. Record a new demo gif. The preferred tool for this is [ttyrec](http://0xcc.net/ttyrec/) and
|
||||
[ttygif](https://github.com/icholy/ttygif). [Asciinema](https://asciinema.org/) handles block
|
||||
character height poorly, [termanilizer](https://www.terminalizer.com/) takes forever to render,
|
||||
[vhs](https://github.com/charmbracelet/vhs) handles braille
|
||||
characters poorly (though if <https://github.com/charmbracelet/vhs/issues/322> is fixed, then
|
||||
it's probably the best option).
|
||||
1. Record a new demo gif if necessary. The preferred tool for this is
|
||||
[vhs](https://github.com/charmbracelet/vhs) (installation instructions in README).
|
||||
|
||||
```shell
|
||||
cargo build --example demo
|
||||
ttyrec -e 'cargo --quiet run --release --example demo -- --tick-rate 100' demo.rec
|
||||
ttygif demo.rec
|
||||
vhs examples/demo.tape --publish --quiet
|
||||
```
|
||||
|
||||
Then upload it somewhere (e.g. use `vhs publish tty.gif` to publish it or upload it to a GitHub
|
||||
wiki page as an attachment). Avoid adding the gif to the git repo as binary files tend to bloat
|
||||
repositories.
|
||||
Then update the link in the [examples README](./examples/README) and the main README. Avoid
|
||||
adding the gif to the git repo as binary files tend to bloat repositories.
|
||||
|
||||
1. Bump the version in [Cargo.toml](Cargo.toml).
|
||||
1. Bump versions in the doc comments of [lib.rs](src/lib.rs).
|
||||
1. Ensure [CHANGELOG.md](CHANGELOG.md) is updated. [git-cliff](https://github.com/orhun/git-cliff)
|
||||
can be used for generating the entries.
|
||||
1. Commit and push the changes.
|
||||
1. Create a new tag: `git tag -a v[X.Y.Z]`
|
||||
1. Push the tag: `git push --tags`
|
||||
1. Wait for [Continuous Deployment](https://github.com/tui-rs-revival/ratatui/actions) workflow to
|
||||
1. Wait for [Continuous Deployment](https://github.com/ratatui-org/ratatui/actions) workflow to
|
||||
finish.
|
||||
|
||||
43
bacon.toml
43
bacon.toml
@@ -15,6 +15,18 @@ need_stdout = false
|
||||
command = ["cargo", "check", "--all-targets", "--all-features", "--color", "always"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-crossterm]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "crossterm"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-termion]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termion"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-termwiz]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termwiz"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.clippy]
|
||||
command = [
|
||||
"cargo", "clippy",
|
||||
@@ -58,27 +70,22 @@ env.RUSTDOCFLAGS = "--cfg docsrs"
|
||||
need_stdout = false
|
||||
on_success = "job:doc" # so that we don't open the browser at each change
|
||||
|
||||
# You can run your application and have the result displayed in bacon,
|
||||
# *if* it makes sense for this crate. You can run an example the same
|
||||
# way. Don't forget the `--color always` part or the errors won't be
|
||||
# properly parsed.
|
||||
[jobs.run]
|
||||
[jobs.coverage]
|
||||
command = [
|
||||
"cargo", "run",
|
||||
"cargo", "llvm-cov",
|
||||
"--lcov", "--output-path", "target/lcov.info",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
# put launch parameters for your program behind a `--` separator
|
||||
]
|
||||
need_stdout = true
|
||||
allow_warnings = true
|
||||
|
||||
[jobs.check-crossterm]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "crossterm"]
|
||||
|
||||
[jobs.check-termion]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termion"]
|
||||
|
||||
[jobs.check-termwiz]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termwiz"]
|
||||
[jobs.coverage-unit-tests-only]
|
||||
command = [
|
||||
"cargo", "llvm-cov",
|
||||
"--lcov", "--output-path", "target/lcov.info",
|
||||
"--lib",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
]
|
||||
|
||||
# You may define here keybindings that would be specific to
|
||||
# a project, for example a shortcut to launch a specific job.
|
||||
@@ -89,3 +96,5 @@ command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default
|
||||
ctrl-c = "job:check-crossterm"
|
||||
ctrl-t = "job:check-termion"
|
||||
ctrl-w = "job:check-termwiz"
|
||||
v = "job:coverage"
|
||||
u = "job:coverage-unit-tests-only"
|
||||
|
||||
73
benches/barchart.rs
Normal file
73
benches/barchart.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use criterion::{criterion_group, criterion_main, Bencher, BenchmarkId, Criterion};
|
||||
use rand::Rng;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
prelude::Direction,
|
||||
widgets::{Bar, BarChart, BarGroup, Widget},
|
||||
};
|
||||
|
||||
/// Benchmark for rendering a barchart.
|
||||
pub fn barchart(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("barchart");
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
for data_count in [64, 256, 2048] {
|
||||
let data: Vec<Bar> = (0..data_count)
|
||||
.map(|i| {
|
||||
Bar::default()
|
||||
.label(format!("B{i}").into())
|
||||
.value(rng.gen_range(0..data_count))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let bargroup = BarGroup::default().bars(&data);
|
||||
|
||||
// Render a basic barchart
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render", data_count),
|
||||
&BarChart::default().data(bargroup.clone()),
|
||||
render,
|
||||
);
|
||||
|
||||
// Render an horizontal barchart
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render_horizontal", data_count),
|
||||
&BarChart::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.data(bargroup.clone()),
|
||||
render,
|
||||
);
|
||||
|
||||
// Render a barchart with multiple groups
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render_grouped", data_count),
|
||||
&BarChart::default()
|
||||
// We call `data` multiple time to add multiple groups.
|
||||
// This is not a duplicated call.
|
||||
.data(bargroup.clone())
|
||||
.data(bargroup.clone())
|
||||
.data(bargroup.clone()),
|
||||
render,
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Render the widget in a classical size buffer
|
||||
fn render(bencher: &mut Bencher, barchart: &BarChart) {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
|
||||
// We use `iter_batched` to clone the value in the setup function.
|
||||
// See https://github.com/ratatui-org/ratatui/pull/377.
|
||||
bencher.iter_batched(
|
||||
|| barchart.clone(),
|
||||
|bench_barchart| {
|
||||
bench_barchart.render(buffer.area, &mut buffer);
|
||||
},
|
||||
criterion::BatchSize::LargeInput,
|
||||
)
|
||||
}
|
||||
|
||||
criterion_group!(benches, barchart);
|
||||
criterion_main!(benches);
|
||||
64
benches/block.rs
Normal file
64
benches/block.rs
Normal 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);
|
||||
73
benches/list.rs
Normal file
73
benches/list.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, Bencher, BenchmarkId, Criterion};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
widgets::{List, ListItem, ListState, StatefulWidget, Widget},
|
||||
};
|
||||
|
||||
/// Benchmark for rendering a list.
|
||||
/// It only benchmarks the render with a different amount of items.
|
||||
pub fn list(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("list");
|
||||
|
||||
for line_count in [64, 2048, 16384] {
|
||||
let lines: Vec<ListItem> = (0..line_count)
|
||||
.map(|_| ListItem::new(fakeit::words::sentence(10)))
|
||||
.collect();
|
||||
|
||||
// Render default list
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render", line_count),
|
||||
&List::new(lines.clone()),
|
||||
render,
|
||||
);
|
||||
|
||||
// Render with an offset to the middle of the list and a selected item
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render_scroll_half", line_count),
|
||||
&List::new(lines.clone()).highlight_symbol(">>"),
|
||||
|b, list| {
|
||||
render_stateful(
|
||||
b,
|
||||
list,
|
||||
ListState::default()
|
||||
.with_offset(line_count / 2)
|
||||
.with_selected(Some(line_count / 2)),
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// render the list into a common size buffer
|
||||
fn render(bencher: &mut Bencher, list: &List) {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
|
||||
// We use `iter_batched` to clone the value in the setup function.
|
||||
// See https://github.com/ratatui-org/ratatui/pull/377.
|
||||
bencher.iter_batched(
|
||||
|| list.to_owned(),
|
||||
|bench_list| {
|
||||
Widget::render(bench_list, buffer.area, &mut buffer);
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
)
|
||||
}
|
||||
|
||||
/// render the list into a common size buffer with a state
|
||||
fn render_stateful(bencher: &mut Bencher, list: &List, mut state: ListState) {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
|
||||
// We use `iter_batched` to clone the value in the setup function.
|
||||
// See https://github.com/ratatui-org/ratatui/pull/377.
|
||||
bencher.iter_batched(
|
||||
|| list.to_owned(),
|
||||
|bench_list| {
|
||||
StatefulWidget::render(bench_list, buffer.area, &mut buffer, &mut state);
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
)
|
||||
}
|
||||
|
||||
criterion_group!(benches, list);
|
||||
criterion_main!(benches);
|
||||
@@ -1,4 +1,6 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, Bencher, BenchmarkId, Criterion};
|
||||
use criterion::{
|
||||
black_box, criterion_group, criterion_main, BatchSize, Bencher, BenchmarkId, Criterion,
|
||||
};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
@@ -69,9 +71,15 @@ pub fn paragraph(c: &mut Criterion) {
|
||||
/// render the paragraph into a buffer with the given width
|
||||
fn render(bencher: &mut Bencher, paragraph: &Paragraph, width: u16) {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, width, 50));
|
||||
bencher.iter(|| {
|
||||
paragraph.clone().render(buffer.area, &mut buffer);
|
||||
})
|
||||
// We use `iter_batched` to clone the value in the setup function.
|
||||
// See https://github.com/ratatui-org/ratatui/pull/377.
|
||||
bencher.iter_batched(
|
||||
|| paragraph.to_owned(),
|
||||
|bench_paragraph| {
|
||||
bench_paragraph.render(buffer.area, &mut buffer);
|
||||
},
|
||||
BatchSize::LargeInput,
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a string with the given number of lines filled with nonsense words
|
||||
|
||||
45
benches/sparkline.rs
Normal file
45
benches/sparkline.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use criterion::{criterion_group, criterion_main, Bencher, BenchmarkId, Criterion};
|
||||
use rand::Rng;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
widgets::{Sparkline, Widget},
|
||||
};
|
||||
|
||||
/// Benchmark for rendering a sparkline.
|
||||
pub fn sparkline(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("sparkline");
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
for data_count in [64, 256, 2048] {
|
||||
let data: Vec<u64> = (0..data_count)
|
||||
.map(|_| rng.gen_range(0..data_count))
|
||||
.collect();
|
||||
|
||||
// Render a basic sparkline
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render", data_count),
|
||||
&Sparkline::default().data(&data),
|
||||
render,
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// render the block into a buffer of the given `size`
|
||||
fn render(bencher: &mut Bencher, sparkline: &Sparkline) {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
|
||||
// We use `iter_batched` to clone the value in the setup function.
|
||||
// See https://github.com/ratatui-org/ratatui/pull/377.
|
||||
bencher.iter_batched(
|
||||
|| sparkline.clone(),
|
||||
|bench_sparkline| {
|
||||
bench_sparkline.render(buffer.area, &mut buffer);
|
||||
},
|
||||
criterion::BatchSize::LargeInput,
|
||||
)
|
||||
}
|
||||
|
||||
criterion_group!(benches, sparkline);
|
||||
criterion_main!(benches);
|
||||
107
cliff.toml
107
cliff.toml
@@ -3,35 +3,51 @@
|
||||
[changelog]
|
||||
# changelog header
|
||||
header = """
|
||||
# Changelog\n
|
||||
All notable changes to this project will be documented in this file.\n
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://tera.netlify.app/docs/#introduction
|
||||
# https://keats.github.io/tera/docs/#introduction
|
||||
# note that the - before / after the % controls whether whitespace is rendered between each line.
|
||||
# Getting this right so that the markdown renders with the correct number of lines between headings
|
||||
# code fences and list items is pretty finicky. Note also that the 4 backticks in the commit macro
|
||||
# is intentional as this escapes any backticks in the commit body.
|
||||
body = """
|
||||
{% if version %}\
|
||||
## {{ version }} - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
{%- if not version %}
|
||||
## [unreleased]
|
||||
{% else -%}
|
||||
## [{{ version }}](https://github.com/ratatui-org/ratatui/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% endif -%}
|
||||
|
||||
{% macro commit(commit) -%}
|
||||
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui-org/ratatui/commit/" ~ commit.id }})
|
||||
*({{commit.scope | default(value = "uncategorized")}})* {{ commit.message | upper_first }}
|
||||
{%- if commit.breaking %} [**breaking**]{% endif %}
|
||||
{%- if commit.body %}
|
||||
|
||||
````text {#- 4 backticks escape any backticks in body #}
|
||||
{{commit.body | indent(prefix=" ") }}
|
||||
````
|
||||
{%- endif %}
|
||||
{% endmacro -%}
|
||||
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | striptags | trim | upper_first }}
|
||||
{% for commit in commits
|
||||
| filter(attribute="scope")
|
||||
| sort(attribute="scope") %}
|
||||
- *({{commit.scope}})* {{ commit.message | upper_first }}{% if commit.breaking %} [**breaking**]{% endif %}
|
||||
{%- endfor -%}
|
||||
{% raw %}\n{% endraw %}\
|
||||
{%- for commit in commits %}
|
||||
{%- if commit.scope -%}
|
||||
{% else -%}
|
||||
- *(uncategorized)* {{ commit.message | upper_first }}{% if commit.breaking %} [**breaking**]{% endif %}
|
||||
{% endif -%}
|
||||
{% endfor -%}
|
||||
{% endfor %}\n
|
||||
### {{ group | striptags | trim | upper_first }}
|
||||
{% for commit in commits | filter(attribute="scope") | sort(attribute="scope") %}
|
||||
{{ self::commit(commit=commit) }}
|
||||
{%- endfor -%}
|
||||
{% for commit in commits %}
|
||||
{%- if not commit.scope %}
|
||||
{{ self::commit(commit=commit) }}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- endfor %}
|
||||
"""
|
||||
|
||||
|
||||
# remove the leading and trailing whitespace from the template
|
||||
trim = true
|
||||
trim = false
|
||||
# changelog footer
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
@@ -46,29 +62,32 @@ filter_unconventional = true
|
||||
split_commits = false
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/tui-rs-revival/ratatui/issues/${2}))" },
|
||||
{ pattern = '(better safe shared layout cache)', replace = "perf(layout): ${1}" },
|
||||
{ pattern = '(Clarify README.md)', replace = "docs(readme): ${1}" },
|
||||
{ pattern = '(Update README.md)', replace = "docs(readme): ${1}" },
|
||||
{ pattern = '(fix typos|Fix typos)', replace = "fix: ${1}" },
|
||||
{ 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 = '(Clarify README.md)', replace = "docs(readme): ${1}" },
|
||||
{ pattern = '(Update README.md)', replace = "docs(readme): ${1}" },
|
||||
{ pattern = '(fix typos|Fix typos)', replace = "fix: ${1}" },
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "<!-- 00 -->Features" },
|
||||
{ message = "^[fF]ix", group = "<!-- 01 -->Bug Fixes" },
|
||||
{ message = "^refactor", group = "<!-- 02 -->Refactor" },
|
||||
{ message = "^doc", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "^perf", group = "<!-- 04 -->Performance" },
|
||||
{ message = "^style", group = "<!-- 05 -->Styling" },
|
||||
{ message = "^test", group = "<!-- 06 -->Testing" },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^chore\\(pr\\)", skip = true },
|
||||
{ message = "^chore\\(pull\\)", skip = true },
|
||||
{ message = "^chore", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ body = ".*security", group = "<!-- 08 -->Security" },
|
||||
{ message = "^build", group = "<!-- 09 -->Build" },
|
||||
{ message = "^ci", group = "<!-- 10 -->Continuous Integration" },
|
||||
{ message = "^revert", group = "<!-- 11 -->Reverted Commits" },
|
||||
{ message = "^feat", group = "<!-- 00 -->Features" },
|
||||
{ message = "^[fF]ix", group = "<!-- 01 -->Bug Fixes" },
|
||||
{ message = "^refactor", group = "<!-- 02 -->Refactor" },
|
||||
{ message = "^doc", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "^perf", group = "<!-- 04 -->Performance" },
|
||||
{ message = "^style", group = "<!-- 05 -->Styling" },
|
||||
{ message = "^test", group = "<!-- 06 -->Testing" },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^chore\\(pr\\)", skip = true },
|
||||
{ message = "^chore\\(pull\\)", skip = true },
|
||||
{ message = "^[cC]hore", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ body = ".*security", group = "<!-- 08 -->Security" },
|
||||
{ message = "^build", group = "<!-- 09 -->Build" },
|
||||
{ message = "^ci", group = "<!-- 10 -->Continuous Integration" },
|
||||
{ message = "^revert", group = "<!-- 11 -->Reverted Commits" },
|
||||
# handle some old commits styles from pre 0.4
|
||||
{ message = "^(Buffer|buffer|Frame|frame|Gauge|gauge|Paragraph|paragraph):", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^\\[", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
]
|
||||
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
||||
protect_breaking_commits = false
|
||||
@@ -79,7 +98,7 @@ tag_pattern = "v[0-9]*"
|
||||
# regex for skipping tags
|
||||
skip_tags = "v0.1.0-rc.1"
|
||||
# regex for ignoring tags
|
||||
ignore_tags = ""
|
||||
ignore_tags = "alpha"
|
||||
# sort the tags topologically
|
||||
topo_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
|
||||
3
codecov.yml
Normal file
3
codecov.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
ignore:
|
||||
- "examples"
|
||||
- "benches"
|
||||
304
examples/README.md
Normal file
304
examples/README.md
Normal file
@@ -0,0 +1,304 @@
|
||||
# Examples
|
||||
|
||||
These gifs were created using [Charm VHS](https://github.com/charmbracelet/vhs).
|
||||
|
||||
VHS has a problem rendering some background color transitions, which shows up in several examples
|
||||
below. See <https://github.com/charmbracelet/vhs/issues/344> for more info. These problems don't
|
||||
occur in a terminal.
|
||||
|
||||
## Demo
|
||||
|
||||
This is the demo example from the main README. It is available for each of the backends. Source:
|
||||
[demo.rs](./demo/).
|
||||
|
||||
```shell
|
||||
cargo run --example=demo --features=crossterm
|
||||
cargo run --example=demo --no-default-features --features=termion
|
||||
cargo run --example=demo --no-default-features --features=termwiz
|
||||
```
|
||||
|
||||
![Demo][demo.gif]
|
||||
|
||||
## Hello World
|
||||
|
||||
This is a pretty boring example, but it contains some good documentation
|
||||
on writing tui apps. Source: [hello_world.rs](./hello_world.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=hello_world --features=crossterm
|
||||
```
|
||||
|
||||
![Hello World][hello_world.gif]
|
||||
|
||||
## Barchart
|
||||
|
||||
Demonstrates the [`BarChart`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
|
||||
widget. Source: [barchart.rs](./barchart.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=barchart --features=crossterm
|
||||
```
|
||||
|
||||
![Barchart][barchart.gif]
|
||||
|
||||
## Block
|
||||
|
||||
Demonstrates the [`Block`](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html)
|
||||
widget. Source: [block.rs](./block.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=block --features=crossterm
|
||||
```
|
||||
|
||||
![Block][block.gif]
|
||||
|
||||
## Calendar
|
||||
|
||||
Demonstrates the [`Calendar`](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
|
||||
widget. Source: [calendar.rs](./calendar.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=calendar --features=crossterm widget-calendar
|
||||
```
|
||||
|
||||
![Calendar][calendar.gif]
|
||||
|
||||
## Canvas
|
||||
|
||||
Demonstrates the [`Canvas`](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html) widget
|
||||
and related shapes in the
|
||||
[`canvas`](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html) module. Source:
|
||||
[canvas.rs](./canvas.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=canvas --features=crossterm
|
||||
```
|
||||
|
||||
![Canvas][canvas.gif]
|
||||
|
||||
## Chart
|
||||
|
||||
Demonstrates the [`Chart`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Chart.html) widget.
|
||||
Source: [chart.rs](./chart.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=chart --features=crossterm
|
||||
```
|
||||
|
||||
![Chart][chart.gif]
|
||||
|
||||
## Colors
|
||||
|
||||
Demonstrates the available [`Color`](https://docs.rs/ratatui/latest/ratatui/style/enum.Color.html)
|
||||
options. These can be used in any style field. Source: [colors.rs](./colors.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=colors --features=crossterm
|
||||
```
|
||||
|
||||
![Colors][colors.gif]
|
||||
|
||||
## Custom Widget
|
||||
|
||||
Demonstrates how to implement the
|
||||
[`Widget`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.Widget.html) trait. Source:
|
||||
[custom_widget.rs](./custom_widget.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=custom_widget --features=crossterm
|
||||
```
|
||||
|
||||
![Custom Widget][custom_widget.gif]
|
||||
|
||||
## Gauge
|
||||
|
||||
Demonstrates the [`Gauge`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Gauge.html) widget.
|
||||
Source: [gauge.rs](./gauge.rs).
|
||||
|
||||
> [!NOTE] The backgrounds render poorly when we generate this example using VHS. This problem
|
||||
> doesn't generally happen during normal rendering in a terminal. See
|
||||
> [vhs#344](https://github.com/charmbracelet/vhs/issues/344) for more details.
|
||||
|
||||
```shell
|
||||
cargo run --example=gauge --features=crossterm
|
||||
```
|
||||
|
||||
![Gauge][gauge.gif]
|
||||
|
||||
## Inline
|
||||
|
||||
Demonstrates the
|
||||
[`Inline`](https://docs.rs/ratatui/latest/ratatui/terminal/enum.Viewport.html#variant.Inline)
|
||||
Viewport mode for ratatui apps. Source: [inline.rs](./inline.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=inline --features=crossterm
|
||||
```
|
||||
|
||||
![Inline][inline.gif]
|
||||
|
||||
## Layout
|
||||
|
||||
Demonstrates the [`Layout`](https://docs.rs/ratatui/latest/ratatui/layout/struct.Layout.html) and
|
||||
interaction between each constraint. Source: [layout.rs](./layout.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=layout --features=crossterm
|
||||
```
|
||||
|
||||
![Layout][layout.gif]
|
||||
|
||||
## List
|
||||
|
||||
Demonstrates the [`List`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html) widget.
|
||||
Source: [list.rs](./list.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=list --features=crossterm
|
||||
```
|
||||
|
||||
![List][list.gif]
|
||||
|
||||
## Modifiers
|
||||
|
||||
Demonstrates the style
|
||||
[`Modifiers`](https://docs.rs/ratatui/latest/ratatui/style/struct.Modifier.html). Source:
|
||||
[modifiers.rs](./modifiers.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=modifiers --features=crossterm
|
||||
```
|
||||
|
||||
![Modifiers][modifiers.gif]
|
||||
|
||||
## Panic
|
||||
|
||||
Demonstrates how to handle panics by ensuring that panic messages are written correctly to the
|
||||
screen. Source: [panic.rs](./panic.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=panic --features=crossterm
|
||||
```
|
||||
|
||||
![Panic][panic.gif]
|
||||
|
||||
## Paragraph
|
||||
|
||||
Demonstrates the [`Paragraph`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Paragraph.html)
|
||||
widget. Source: [paragraph.rs](./paragraph.rs)
|
||||
|
||||
```shell
|
||||
cargo run --example=paragraph --features=crossterm
|
||||
```
|
||||
|
||||
![Paragraph][paragraph.gif]
|
||||
|
||||
## Popup
|
||||
|
||||
Demonstrates how to render a widget over the top of previously rendered widgets using the
|
||||
[`Clear`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Clear.html) widget. Source:
|
||||
[popup.rs](./popup.rs).
|
||||
|
||||
>
|
||||
```shell
|
||||
cargo run --example=popup --features=crossterm
|
||||
```
|
||||
|
||||
> [!NOTE] The background renders poorly after the popup when we generate this example using VHS.
|
||||
> This problem doesn't generally happen during normal rendering in a terminal. See
|
||||
> [vhs#344](https://github.com/charmbracelet/vhs/issues/344) for more details.
|
||||
|
||||
![Popup][popup.gif]
|
||||
|
||||
## Scrollbar
|
||||
|
||||
Demonstrates the [`Scrollbar`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Scrollbar.html)
|
||||
widget. Source: [scrollbar.rs](./scrollbar.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=scrollbar --features=crossterm
|
||||
```
|
||||
|
||||
![Scrollbar][scrollbar.gif]
|
||||
|
||||
## Sparkline
|
||||
|
||||
Demonstrates the [`Sparkline`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Sparkline.html)
|
||||
widget. Source: [sparkline.rs](./sparkline.rs).
|
||||
|
||||
> [!NOTE] The background renders poorly in the second sparkline when we generate this example using
|
||||
> VHS. This problem doesn't generally happen during normal rendering in a terminal. See
|
||||
> [vhs#344](https://github.com/charmbracelet/vhs/issues/344) for more details.
|
||||
|
||||
```shell
|
||||
cargo run --example=sparkline --features=crossterm
|
||||
```
|
||||
|
||||
![Sparkline][sparkline.gif]
|
||||
|
||||
## Table
|
||||
|
||||
Demonstrates the [`Table`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Table.html) widget.
|
||||
Source: [table.rs](./table.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=table --features=crossterm
|
||||
```
|
||||
|
||||
![Table][table.gif]
|
||||
|
||||
## Tabs
|
||||
|
||||
Demonstrates the [`Tabs`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Tabs.html) widget.
|
||||
Source: [tabs.rs](./tabs.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=tabs --features=crossterm
|
||||
```
|
||||
|
||||
![Tabs][tabs.gif]
|
||||
|
||||
## User Input
|
||||
|
||||
Demonstrates one approach to accepting user input. Source [user_input.rs](./user_input.rs).
|
||||
|
||||
> [!NOTE] Consider using [`tui-textarea`](https://crates.io/crates/tui-textarea) or
|
||||
> [`tui-input`](https://crates.io/crates/tui-input) crates for more functional text entry UIs.
|
||||
|
||||
```shell
|
||||
cargo run --example=user_input --features=crossterm
|
||||
```
|
||||
|
||||
![User Input][user_input.gif]
|
||||
|
||||
<!--
|
||||
links to images to make it easier to update in bulk
|
||||
These are generated with `vhs publish examples/xxx.gif`
|
||||
|
||||
To update these examples in bulk:
|
||||
```shell
|
||||
examples/generate.bash
|
||||
```
|
||||
-->
|
||||
[barchart.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/barchart.gif?raw=true
|
||||
[block.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/block.gif?raw=true
|
||||
[calendar.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/calendar.gif?raw=true
|
||||
[canvas.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/canvas.gif?raw=true
|
||||
[chart.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/chart.gif?raw=true
|
||||
[colors.gif]: https://github.com/ratatui-org/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
|
||||
[demo.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/demo.gif?raw=true
|
||||
[gauge.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/gauge.gif?raw=true
|
||||
[hello_world.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/hello_world.gif?raw=true
|
||||
[inline.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/inline.gif?raw=true
|
||||
[layout.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/layout.gif?raw=true
|
||||
[list.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/list.gif?raw=true
|
||||
[modifiers.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/modifiers.gif?raw=true
|
||||
[panic.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/panic.gif?raw=true
|
||||
[paragraph.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/paragraph.gif?raw=true
|
||||
[popup.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/popup.gif?raw=true
|
||||
[scrollbar.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/scrollbar.gif?raw=true
|
||||
[sparkline.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/sparkline.gif?raw=true
|
||||
[table.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/table.gif?raw=true
|
||||
[tabs.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/tabs.gif?raw=true
|
||||
[user_input.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/user_input.gif?raw=true
|
||||
@@ -9,18 +9,22 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{BarChart, Block, Borders},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
struct Company<'a> {
|
||||
revenue: [u64; 4],
|
||||
label: &'a str,
|
||||
bar_style: Style,
|
||||
}
|
||||
|
||||
struct App<'a> {
|
||||
data: Vec<(&'a str, u64)>,
|
||||
months: [&'a str; 4],
|
||||
companies: [Company<'a>; 3],
|
||||
}
|
||||
|
||||
const TOTAL_REVENUE: &str = "Total Revenue";
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
@@ -50,6 +54,24 @@ impl<'a> App<'a> {
|
||||
("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"],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,9 +139,9 @@ fn run_app<B: Backend>(
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().title("Data1").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
@@ -133,30 +155,139 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[1]);
|
||||
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().title("Data2").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.bar_width(5)
|
||||
.bar_gap(3)
|
||||
.bar_style(Style::default().fg(Color::Green))
|
||||
.value_style(
|
||||
Style::default()
|
||||
.bg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
f.render_widget(barchart, chunks[0]);
|
||||
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().title("Data3").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.bar_style(Style::default().fg(Color::Red))
|
||||
.bar_width(7)
|
||||
.bar_gap(0)
|
||||
.value_style(Style::default().bg(Color::Red))
|
||||
.label_style(
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
);
|
||||
f.render_widget(barchart, chunks[1]);
|
||||
draw_bar_with_group_labels(f, app, chunks[0]);
|
||||
draw_horizontal_bars(f, app, chunks[1]);
|
||||
}
|
||||
|
||||
fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec<BarGroup<'a>> {
|
||||
app.months
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &month)| {
|
||||
let bars: Vec<Bar> = app
|
||||
.companies
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let mut bar = Bar::default()
|
||||
.value(c.revenue[i])
|
||||
.style(c.bar_style)
|
||||
.value_style(
|
||||
Style::default()
|
||||
.bg(c.bar_style.fg.unwrap())
|
||||
.fg(Color::Black),
|
||||
);
|
||||
|
||||
if combine_values_and_labels {
|
||||
bar = bar.text_value(format!(
|
||||
"{} ({:.1} M)",
|
||||
c.label,
|
||||
(c.revenue[i] as f64) / 1000.
|
||||
));
|
||||
} else {
|
||||
bar = bar
|
||||
.text_value(format!("{:.1}", (c.revenue[i] as f64) / 1000.))
|
||||
.label(c.label.into());
|
||||
}
|
||||
bar
|
||||
})
|
||||
.collect();
|
||||
BarGroup::default()
|
||||
.label(Line::from(month).alignment(Alignment::Center))
|
||||
.bars(&bars)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn draw_bar_with_group_labels<B>(f: &mut Frame<B>, app: &App, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
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<B>(f: &mut Frame<B>, app: &App, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let groups = create_groups(app, true);
|
||||
|
||||
let mut barchart = BarChart::default()
|
||||
.block(Block::default().title("Data1").borders(Borders::ALL))
|
||||
.bar_width(1)
|
||||
.group_gap(1)
|
||||
.bar_gap(0)
|
||||
.direction(Direction::Horizontal);
|
||||
|
||||
for group in groups {
|
||||
barchart = barchart.data(group)
|
||||
}
|
||||
|
||||
f.render_widget(barchart, area);
|
||||
|
||||
const LEGEND_HEIGHT: u16 = 6;
|
||||
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
|
||||
let legend_width = TOTAL_REVENUE.len() as u16 + 2;
|
||||
let legend_area = Rect {
|
||||
height: LEGEND_HEIGHT,
|
||||
width: legend_width,
|
||||
y: area.y,
|
||||
x: area.right() - legend_width,
|
||||
};
|
||||
draw_legend(f, legend_area);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_legend<B>(f: &mut Frame<B>, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let text = vec![
|
||||
Line::from(Span::styled(
|
||||
TOTAL_REVENUE,
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::White),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
"- Company A",
|
||||
Style::default().fg(Color::Green),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
"- Company B",
|
||||
Style::default().fg(Color::Yellow),
|
||||
)),
|
||||
Line::from(vec![Span::styled(
|
||||
"- Company C",
|
||||
Style::default().fg(Color::White),
|
||||
)]),
|
||||
];
|
||||
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(Color::White));
|
||||
let paragraph = Paragraph::new(text).block(block);
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
12
examples/barchart.tape
Normal file
12
examples/barchart.tape
Normal file
@@ -0,0 +1,12 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/barchart.tape`
|
||||
Output "target/barchart.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 800
|
||||
Hide
|
||||
Type "cargo run --example=barchart"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 5s
|
||||
@@ -1,126 +1,253 @@
|
||||
use std::{error::Error, io};
|
||||
use std::{
|
||||
error::Error,
|
||||
io::{stdout, Stdout},
|
||||
ops::ControlFlow,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Style, Stylize},
|
||||
widgets::{block::title::Title, Block, BorderType, Borders, Padding, Paragraph},
|
||||
Frame, Terminal,
|
||||
prelude::*,
|
||||
widgets::{
|
||||
block::{Position, Title},
|
||||
Block, BorderType, Borders, Padding, Paragraph, Wrap,
|
||||
},
|
||||
};
|
||||
|
||||
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)?;
|
||||
// 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 Frame<'a> = ratatui::Frame<'a, CrosstermBackend<Stdout>>;
|
||||
type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
|
||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
|
||||
// create app and run it
|
||||
let res = run_app(&mut terminal);
|
||||
fn main() -> Result<()> {
|
||||
let mut terminal = setup_terminal()?;
|
||||
let result = run(&mut terminal);
|
||||
restore_terminal(terminal)?;
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
if let Err(err) = result {
|
||||
eprintln!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::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 {
|
||||
terminal.draw(ui)?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
if handle_events()?.is_break() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>) {
|
||||
// Wrapping block for a group
|
||||
// Just draw the block and the group on the same area and build the group
|
||||
// with at least a margin of 1
|
||||
let size = f.size();
|
||||
|
||||
// Surrounding block
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(Title::from("Main block with round corners").alignment(Alignment::Center))
|
||||
.border_type(BorderType::Rounded);
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(4)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
// Top two inner blocks
|
||||
let top_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[0]);
|
||||
|
||||
// Top left inner block with green background
|
||||
let block = Block::default()
|
||||
.title(vec!["With".yellow(), " background".into()])
|
||||
.on_green();
|
||||
f.render_widget(block, top_chunks[0]);
|
||||
|
||||
// Top right inner block with styled title aligned to the right
|
||||
let block = Block::default()
|
||||
.title(Title::from("Styled title".white().on_red().bold()).alignment(Alignment::Right));
|
||||
f.render_widget(block, top_chunks[1]);
|
||||
|
||||
// Bottom two inner blocks
|
||||
let bottom_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[1]);
|
||||
|
||||
// Bottom left block with all default borders
|
||||
let block = Block::default()
|
||||
.title("With borders")
|
||||
.borders(Borders::ALL)
|
||||
.padding(Padding {
|
||||
left: 4,
|
||||
right: 4,
|
||||
top: 2,
|
||||
bottom: 2,
|
||||
});
|
||||
|
||||
let text = Paragraph::new("text inside padded block").block(block);
|
||||
f.render_widget(text, bottom_chunks[0]);
|
||||
|
||||
// Bottom right block with styled left and right border
|
||||
let block = Block::default()
|
||||
.title("With styled borders and doubled borders")
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.border_type(BorderType::Double)
|
||||
.padding(Padding::uniform(1));
|
||||
|
||||
let inner_block = Block::default()
|
||||
.title("Block inside padded block")
|
||||
.borders(Borders::ALL);
|
||||
|
||||
let inner_area = block.inner(bottom_chunks[1]);
|
||||
f.render_widget(block, bottom_chunks[1]);
|
||||
f.render_widget(inner_block, inner_area);
|
||||
fn handle_events() -> Result<ControlFlow<()>> {
|
||||
if event::poll(Duration::from_millis(100))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(ControlFlow::Break(()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ControlFlow::Continue(()))
|
||||
}
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
let (title_area, layout) = calculate_layout(frame.size());
|
||||
|
||||
render_title(frame, title_area);
|
||||
|
||||
let paragraph = placeholder_paragraph();
|
||||
|
||||
render_borders(¶graph, Borders::ALL, frame, layout[0][0]);
|
||||
render_borders(¶graph, Borders::NONE, frame, layout[0][1]);
|
||||
render_borders(¶graph, Borders::LEFT, frame, layout[1][0]);
|
||||
render_borders(¶graph, Borders::RIGHT, frame, layout[1][1]);
|
||||
render_borders(¶graph, Borders::TOP, frame, layout[2][0]);
|
||||
render_borders(¶graph, Borders::BOTTOM, frame, layout[2][1]);
|
||||
|
||||
render_border_type(¶graph, BorderType::Plain, frame, layout[3][0]);
|
||||
render_border_type(¶graph, BorderType::Rounded, frame, layout[3][1]);
|
||||
render_border_type(¶graph, BorderType::Double, frame, layout[4][0]);
|
||||
render_border_type(¶graph, BorderType::Thick, frame, layout[4][1]);
|
||||
|
||||
render_styled_block(¶graph, frame, layout[5][0]);
|
||||
render_styled_borders(¶graph, frame, layout[5][1]);
|
||||
render_styled_title(¶graph, frame, layout[6][0]);
|
||||
render_styled_title_content(¶graph, frame, layout[6][1]);
|
||||
render_multiple_titles(¶graph, frame, layout[7][0]);
|
||||
render_multiple_title_positions(¶graph, frame, layout[7][1]);
|
||||
render_padding(¶graph, frame, layout[8][0]);
|
||||
render_nested_blocks(¶graph, frame, layout[8][1]);
|
||||
}
|
||||
|
||||
/// Calculate the layout of the UI elements.
|
||||
///
|
||||
/// Returns a tuple of the title area and the main areas.
|
||||
fn calculate_layout(area: Rect) -> (Rect, Vec<Vec<Rect>>) {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Constraint::Length(1), Constraint::Min(0)])
|
||||
.split(area);
|
||||
let title_area = layout[0];
|
||||
let main_areas = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Constraint::Max(4); 9])
|
||||
.split(layout[1])
|
||||
.iter()
|
||||
.map(|&area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(area)
|
||||
.to_vec()
|
||||
})
|
||||
.collect_vec();
|
||||
(title_area, main_areas)
|
||||
}
|
||||
|
||||
fn render_title(frame: &mut Frame, area: Rect) {
|
||||
frame.render_widget(
|
||||
Paragraph::new("Block example. Press q to quit")
|
||||
.dark_gray()
|
||||
.alignment(Alignment::Center),
|
||||
area,
|
||||
);
|
||||
}
|
||||
|
||||
fn placeholder_paragraph() -> Paragraph<'static> {
|
||||
let text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
|
||||
Paragraph::new(text.dark_gray()).wrap(Wrap { trim: true })
|
||||
}
|
||||
|
||||
fn render_borders(paragraph: &Paragraph, border: Borders, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::new()
|
||||
.borders(border)
|
||||
.title(format!("Borders::{border:#?}", border = border));
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
|
||||
fn render_border_type(
|
||||
paragraph: &Paragraph,
|
||||
border_type: BorderType,
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
) {
|
||||
let block = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(border_type)
|
||||
.title(format!("BorderType::{border_type:#?}"));
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
fn render_styled_borders(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::new().blue().on_white().bold().italic())
|
||||
.title("Styled borders");
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
|
||||
fn render_styled_block(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::new().blue().on_white().bold().italic())
|
||||
.title("Styled block");
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
|
||||
// Note: this currently renders incorrectly, see https://github.com/ratatui-org/ratatui/issues/349
|
||||
fn render_styled_title(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
.title("Styled title")
|
||||
.title_style(Style::new().blue().on_white().bold().italic());
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
|
||||
fn render_styled_title_content(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let title = Line::from(vec![
|
||||
"Styled ".blue().on_white().bold().italic(),
|
||||
"title content".red().on_white().bold().italic(),
|
||||
]);
|
||||
let block = Block::new().borders(Borders::ALL).title(title);
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
|
||||
fn render_multiple_titles(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
.title("Multiple".blue().on_white().bold().italic())
|
||||
.title("Titles".red().on_white().bold().italic());
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
|
||||
fn render_multiple_title_positions(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
.title(
|
||||
Title::from("top left")
|
||||
.position(Position::Top)
|
||||
.alignment(Alignment::Left),
|
||||
)
|
||||
.title(
|
||||
Title::from("top center")
|
||||
.position(Position::Top)
|
||||
.alignment(Alignment::Center),
|
||||
)
|
||||
.title(
|
||||
Title::from("top right")
|
||||
.position(Position::Top)
|
||||
.alignment(Alignment::Right),
|
||||
)
|
||||
.title(
|
||||
Title::from("bottom left")
|
||||
.position(Position::Bottom)
|
||||
.alignment(Alignment::Left),
|
||||
)
|
||||
.title(
|
||||
Title::from("bottom center")
|
||||
.position(Position::Bottom)
|
||||
.alignment(Alignment::Center),
|
||||
)
|
||||
.title(
|
||||
Title::from("bottom right")
|
||||
.position(Position::Bottom)
|
||||
.alignment(Alignment::Right),
|
||||
);
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
|
||||
fn render_padding(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::new()
|
||||
.borders(Borders::ALL)
|
||||
.title("Padding")
|
||||
.padding(Padding::new(5, 10, 1, 2));
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
|
||||
fn render_nested_blocks(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let outer_block = Block::new().borders(Borders::ALL).title("Outer block");
|
||||
let inner_block = Block::new().borders(Borders::ALL).title("Inner block");
|
||||
let inner = outer_block.inner(area);
|
||||
frame.render_widget(outer_block, area);
|
||||
frame.render_widget(paragraph.clone().block(inner_block), inner);
|
||||
}
|
||||
|
||||
12
examples/block.tape
Normal file
12
examples/block.tape
Normal file
@@ -0,0 +1,12 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/block.tape`
|
||||
Output "target/block.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 1200
|
||||
Hide
|
||||
Type "cargo run --example=block"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 2s
|
||||
@@ -5,13 +5,7 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::calendar::{CalendarEventStore, DateStyler, Monthly},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::calendar::*};
|
||||
use time::{Date, Month, OffsetDateTime};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
|
||||
12
examples/calendar.tape
Normal file
12
examples/calendar.tape
Normal file
@@ -0,0 +1,12 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/calendar.tape`
|
||||
Output "target/calendar.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 800
|
||||
Hide
|
||||
Type "cargo run --example=calendar --features=crossterm,widget-calendar"
|
||||
Enter
|
||||
Sleep 3s
|
||||
Show
|
||||
Sleep 5s
|
||||
@@ -10,15 +10,8 @@ use crossterm::{
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Stylize},
|
||||
symbols::Marker,
|
||||
widgets::{
|
||||
canvas::{Canvas, Map, MapResolution, Rectangle},
|
||||
Block, Borders,
|
||||
},
|
||||
Frame, Terminal,
|
||||
prelude::*,
|
||||
widgets::{canvas::*, *},
|
||||
};
|
||||
|
||||
struct App {
|
||||
|
||||
12
examples/canvas.tape
Normal file
12
examples/canvas.tape
Normal file
@@ -0,0 +1,12 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/canvas.tape`
|
||||
Output "target/canvas.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 800
|
||||
Hide
|
||||
Type "cargo run --example=canvas --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 5s
|
||||
@@ -9,15 +9,7 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
symbols,
|
||||
text::Span,
|
||||
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
const DATA: [(f64, f64); 5] = [(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0)];
|
||||
const DATA2: [(f64, f64); 7] = [
|
||||
|
||||
12
examples/chart.tape
Normal file
12
examples/chart.tape
Normal file
@@ -0,0 +1,12 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/chart.tape`
|
||||
Output "target/chart.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 800
|
||||
Hide
|
||||
Type "cargo run --example=chart --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 5s
|
||||
295
examples/colors.rs
Normal file
295
examples/colors.rs
Normal file
@@ -0,0 +1,295 @@
|
||||
/// 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 std::{
|
||||
error::Error,
|
||||
io::{self, Stdout},
|
||||
result,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
type Result<T> = result::Result<T, Box<dyn Error>>;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut terminal = setup_terminal()?;
|
||||
let res = run_app(&mut terminal);
|
||||
restore_terminal(terminal)?;
|
||||
if let Err(err) = res {
|
||||
eprintln!("{err:?}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(ui)?;
|
||||
|
||||
if event::poll(Duration::from_millis(250))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(frame: &mut Frame<B>) {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![
|
||||
Constraint::Length(30),
|
||||
Constraint::Length(17),
|
||||
Constraint::Length(2),
|
||||
])
|
||||
.split(frame.size());
|
||||
|
||||
render_named_colors(frame, layout[0]);
|
||||
render_indexed_colors(frame, layout[1]);
|
||||
render_indexed_grayscale(frame, layout[2]);
|
||||
}
|
||||
|
||||
const NAMED_COLORS: [Color; 16] = [
|
||||
Color::Black,
|
||||
Color::Red,
|
||||
Color::Green,
|
||||
Color::Yellow,
|
||||
Color::Blue,
|
||||
Color::Magenta,
|
||||
Color::Cyan,
|
||||
Color::Gray,
|
||||
Color::DarkGray,
|
||||
Color::LightRed,
|
||||
Color::LightGreen,
|
||||
Color::LightYellow,
|
||||
Color::LightBlue,
|
||||
Color::LightMagenta,
|
||||
Color::LightCyan,
|
||||
Color::White,
|
||||
];
|
||||
|
||||
fn render_named_colors<B: Backend>(frame: &mut Frame<B>, area: Rect) {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Constraint::Length(3); 10])
|
||||
.split(area);
|
||||
|
||||
render_fg_named_colors(frame, Color::Reset, layout[0]);
|
||||
render_fg_named_colors(frame, Color::Black, layout[1]);
|
||||
render_fg_named_colors(frame, Color::DarkGray, layout[2]);
|
||||
render_fg_named_colors(frame, Color::Gray, layout[3]);
|
||||
render_fg_named_colors(frame, Color::White, layout[4]);
|
||||
|
||||
render_bg_named_colors(frame, Color::Reset, layout[5]);
|
||||
render_bg_named_colors(frame, Color::Black, layout[6]);
|
||||
render_bg_named_colors(frame, Color::DarkGray, layout[7]);
|
||||
render_bg_named_colors(frame, Color::Gray, layout[8]);
|
||||
render_bg_named_colors(frame, Color::White, layout[9]);
|
||||
}
|
||||
|
||||
fn render_fg_named_colors<B: Backend>(frame: &mut Frame<B>, bg: Color, area: Rect) {
|
||||
let block = title_block(format!("Foreground colors on {bg} background"));
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Constraint::Length(1); 2])
|
||||
.split(inner)
|
||||
.iter()
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Ratio(1, 8); 8])
|
||||
.split(*area)
|
||||
.to_vec()
|
||||
})
|
||||
.collect_vec();
|
||||
for (i, &fg) in NAMED_COLORS.iter().enumerate() {
|
||||
let color_name = fg.to_string();
|
||||
let paragraph = Paragraph::new(color_name).fg(fg).bg(bg);
|
||||
frame.render_widget(paragraph, layout[i]);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_bg_named_colors<B: Backend>(frame: &mut Frame<B>, fg: Color, area: Rect) {
|
||||
let block = title_block(format!("Background colors with {fg} foreground"));
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Constraint::Length(1); 2])
|
||||
.split(inner)
|
||||
.iter()
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Ratio(1, 8); 8])
|
||||
.split(*area)
|
||||
.to_vec()
|
||||
})
|
||||
.collect_vec();
|
||||
for (i, &bg) in NAMED_COLORS.iter().enumerate() {
|
||||
let color_name = bg.to_string();
|
||||
let paragraph = Paragraph::new(color_name).fg(fg).bg(bg);
|
||||
frame.render_widget(paragraph, layout[i]);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_indexed_colors<B: Backend>(frame: &mut Frame<B>, area: Rect) {
|
||||
let block = title_block("Indexed colors".into());
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![
|
||||
Constraint::Length(1), // 0 - 15
|
||||
Constraint::Length(1), // blank
|
||||
Constraint::Min(6), // 16 - 123
|
||||
Constraint::Length(1), // blank
|
||||
Constraint::Min(6), // 124 - 231
|
||||
Constraint::Length(1), // blank
|
||||
])
|
||||
.split(inner);
|
||||
|
||||
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
||||
let color_layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Length(5); 16])
|
||||
.split(layout[0]);
|
||||
for i in 0..16 {
|
||||
let color = Color::Indexed(i);
|
||||
let color_index = format!("{i:0>2}");
|
||||
let bg = if i < 1 { Color::DarkGray } else { Color::Black };
|
||||
let paragraph = Paragraph::new(Line::from(vec![
|
||||
color_index.fg(color).bg(bg),
|
||||
"██".bg(color).fg(color),
|
||||
]));
|
||||
frame.render_widget(paragraph, color_layout[i as usize]);
|
||||
}
|
||||
|
||||
// 16 17 18 19 20 21 52 53 54 55 56 57 88 89 90 91 92 93
|
||||
// 22 23 24 25 26 27 58 59 60 61 62 63 94 95 96 97 98 99
|
||||
// 28 29 30 31 32 33 64 65 66 67 68 69 100 101 102 103 104 105
|
||||
// 34 35 36 37 38 39 70 71 72 73 74 75 106 107 108 109 110 111
|
||||
// 40 41 42 43 44 45 76 77 78 79 80 81 112 113 114 115 116 117
|
||||
// 46 47 48 49 50 51 82 83 84 85 86 87 118 119 120 121 122 123
|
||||
//
|
||||
// 124 125 126 127 128 129 160 161 162 163 164 165 196 197 198 199 200 201
|
||||
// 130 131 132 133 134 135 166 167 168 169 170 171 202 203 204 205 206 207
|
||||
// 136 137 138 139 140 141 172 173 174 175 176 177 208 209 210 211 212 213
|
||||
// 142 143 144 145 146 147 178 179 180 181 182 183 214 215 216 217 218 219
|
||||
// 148 149 150 151 152 153 184 185 186 187 188 189 220 221 222 223 224 225
|
||||
// 154 155 156 157 158 159 190 191 192 193 194 195 226 227 228 229 230 231
|
||||
|
||||
// the above looks complex but it's so the colors are grouped into blocks that display nicely
|
||||
let index_layout = [layout[2], layout[4]]
|
||||
.iter()
|
||||
// two rows of 3 columns
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Length(27); 3])
|
||||
.split(*area)
|
||||
.to_vec()
|
||||
})
|
||||
// each with 6 rows
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Constraint::Length(1); 6])
|
||||
.split(area)
|
||||
.to_vec()
|
||||
})
|
||||
// each with 6 columns
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Min(4); 6])
|
||||
.split(area)
|
||||
.to_vec()
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
for i in 16..=231 {
|
||||
let color = Color::Indexed(i);
|
||||
let color_index = format!("{i:0>3}");
|
||||
let paragraph = Paragraph::new(Line::from(vec![
|
||||
color_index.fg(color).bg(Color::Reset),
|
||||
".".bg(color).fg(color),
|
||||
// There's a bug in VHS that seems to bleed backgrounds into the next
|
||||
// character. This is a workaround to make the bug less obvious.
|
||||
"███".reversed(),
|
||||
]));
|
||||
frame.render_widget(paragraph, index_layout[i as usize - 16]);
|
||||
}
|
||||
}
|
||||
|
||||
fn title_block(title: String) -> Block<'static> {
|
||||
Block::default()
|
||||
.borders(Borders::TOP)
|
||||
.border_style(Style::new().dark_gray())
|
||||
.title(title)
|
||||
.title_alignment(Alignment::Center)
|
||||
.title_style(Style::new().reset())
|
||||
}
|
||||
|
||||
fn render_indexed_grayscale<B: Backend>(frame: &mut Frame<B>, area: Rect) {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![
|
||||
Constraint::Length(1), // 232 - 243
|
||||
Constraint::Length(1), // 244 - 255
|
||||
])
|
||||
.split(area)
|
||||
.iter()
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Length(6); 12])
|
||||
.split(*area)
|
||||
.to_vec()
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
for i in 232..=255 {
|
||||
let color = Color::Indexed(i);
|
||||
let color_index = format!("{i:0>3}");
|
||||
// make the dark colors easier to read
|
||||
let bg = if i < 244 { Color::Gray } else { Color::Black };
|
||||
let paragraph = Paragraph::new(Line::from(vec![
|
||||
color_index.fg(color).bg(bg),
|
||||
"██".bg(color).fg(color),
|
||||
// There's a bug in VHS that seems to bleed backgrounds into the next
|
||||
// character. This is a workaround to make the bug less obvious.
|
||||
"███████".reversed(),
|
||||
]));
|
||||
frame.render_widget(paragraph, layout[i as usize - 232]);
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
18
examples/colors.tape
Normal file
18
examples/colors.tape
Normal file
@@ -0,0 +1,18 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/colors.tape`
|
||||
Output "target/colors.gif"
|
||||
# The OceanicMaterial theme is a good choice for this example (Obsidian is almost as good) because:
|
||||
# - Black is dark and distinct from the default background
|
||||
# - White is light and distinct from the default foreground
|
||||
# - Normal and bright colors are distinct
|
||||
# - Black and DarkGray are distinct
|
||||
# - White and Gray are distinct
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 1410
|
||||
Hide
|
||||
Type "cargo run --example=colors --features=crossterm"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 1s
|
||||
@@ -5,14 +5,7 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
widgets::Widget,
|
||||
Frame, Terminal,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Label<'a> {
|
||||
|
||||
12
examples/custom_widget.tape
Normal file
12
examples/custom_widget.tape
Normal file
@@ -0,0 +1,12 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/custom_widget.tape`
|
||||
Output "target/custom_widget.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 200
|
||||
Hide
|
||||
Type "cargo run --example=custom_widget --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 5s
|
||||
18
examples/demo.tape
Normal file
18
examples/demo.tape
Normal file
@@ -0,0 +1,18 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/demo.tape`
|
||||
Output "target/demo.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 1200
|
||||
Set PlaybackSpeed 0.5
|
||||
Hide
|
||||
Type "cargo run --example demo"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 1s
|
||||
Down@1s 12
|
||||
Right
|
||||
Sleep 4s
|
||||
Right
|
||||
Sleep 4s
|
||||
@@ -2,7 +2,7 @@ use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use ratatui::widgets::ListState;
|
||||
use ratatui::widgets::*;
|
||||
|
||||
const TASKS: [&str; 24] = [
|
||||
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10",
|
||||
|
||||
@@ -9,10 +9,7 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
Terminal,
|
||||
};
|
||||
use ratatui::prelude::*;
|
||||
|
||||
use crate::{app::App, ui};
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use std::{error::Error, io, sync::mpsc, thread, time::Duration};
|
||||
|
||||
use ratatui::{
|
||||
backend::{Backend, TermionBackend},
|
||||
Terminal,
|
||||
};
|
||||
use ratatui::prelude::*;
|
||||
use termion::{
|
||||
event::Key,
|
||||
input::{MouseTerminal, TermRead},
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ratatui::{backend::TermwizBackend, Terminal};
|
||||
use ratatui::prelude::*;
|
||||
use termwiz::{input::*, terminal::Terminal as TermwizTerminal};
|
||||
|
||||
use crate::{app::App, ui};
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
use ratatui::{
|
||||
backend::Backend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
text::{Line, Span},
|
||||
widgets::{
|
||||
canvas::{Canvas, Circle, Line as CanvasLine, Map, MapResolution, Rectangle},
|
||||
Axis, BarChart, Block, Borders, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem,
|
||||
Paragraph, Row, Sparkline, Table, Tabs, Wrap,
|
||||
},
|
||||
Frame,
|
||||
prelude::*,
|
||||
widgets::{canvas::*, *},
|
||||
};
|
||||
|
||||
use crate::app::App;
|
||||
@@ -22,7 +13,7 @@ pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
.tabs
|
||||
.titles
|
||||
.iter()
|
||||
.map(|t| Line::from(Span::styled(*t, Style::default().fg(Color::Green))))
|
||||
.map(|t| text::Line::from(Span::styled(*t, Style::default().fg(Color::Green))))
|
||||
.collect();
|
||||
let tabs = Tabs::new(titles)
|
||||
.block(Block::default().borders(Borders::ALL).title(app.title))
|
||||
@@ -139,7 +130,7 @@ where
|
||||
.tasks
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| ListItem::new(vec![Line::from(Span::raw(*i))]))
|
||||
.map(|i| ListItem::new(vec![text::Line::from(Span::raw(*i))]))
|
||||
.collect();
|
||||
let tasks = List::new(tasks)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
@@ -163,7 +154,7 @@ where
|
||||
"WARNING" => warning_style,
|
||||
_ => info_style,
|
||||
};
|
||||
let content = vec![Line::from(vec![
|
||||
let content = vec![text::Line::from(vec![
|
||||
Span::styled(format!("{level:<9}"), s),
|
||||
Span::raw(evt),
|
||||
])];
|
||||
@@ -263,9 +254,9 @@ where
|
||||
B: Backend,
|
||||
{
|
||||
let text = vec![
|
||||
Line::from("This is a paragraph with several lines. You can change style your text the way you want"),
|
||||
Line::from(""),
|
||||
Line::from(vec![
|
||||
text::Line::from("This is a paragraph with several lines. You can change style your text the way you want"),
|
||||
text::Line::from(""),
|
||||
text::Line::from(vec![
|
||||
Span::from("For example: "),
|
||||
Span::styled("under", Style::default().fg(Color::Red)),
|
||||
Span::raw(" "),
|
||||
@@ -274,7 +265,7 @@ where
|
||||
Span::styled("rainbow", Style::default().fg(Color::Blue)),
|
||||
Span::raw("."),
|
||||
]),
|
||||
Line::from(vec![
|
||||
text::Line::from(vec![
|
||||
Span::raw("Oh and if you didn't "),
|
||||
Span::styled("notice", Style::default().add_modifier(Modifier::ITALIC)),
|
||||
Span::raw(" you can "),
|
||||
@@ -285,7 +276,7 @@ where
|
||||
Span::styled("text", Style::default().add_modifier(Modifier::UNDERLINED)),
|
||||
Span::raw(".")
|
||||
]),
|
||||
Line::from(
|
||||
text::Line::from(
|
||||
"One more thing is that it should display unicode characters: 10€"
|
||||
),
|
||||
];
|
||||
@@ -356,7 +347,7 @@ where
|
||||
});
|
||||
for (i, s1) in app.servers.iter().enumerate() {
|
||||
for s2 in &app.servers[i + 1..] {
|
||||
ctx.draw(&CanvasLine {
|
||||
ctx.draw(&canvas::Line {
|
||||
x1: s1.coords.1,
|
||||
y1: s1.coords.0,
|
||||
y2: s2.coords.0,
|
||||
|
||||
@@ -9,14 +9,7 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
widgets::{Block, Borders, Gauge},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
struct App {
|
||||
progress1: u16,
|
||||
@@ -113,7 +106,6 @@ fn run_app<B: Backend>(
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
@@ -155,7 +147,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
|
||||
let label = format!("{}/100", app.progress4);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge4"))
|
||||
.block(Block::default().title("Gauge4").borders(Borders::ALL))
|
||||
.gauge_style(
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
|
||||
12
examples/gauge.tape
Normal file
12
examples/gauge.tape
Normal file
@@ -0,0 +1,12 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/gauge.tape`
|
||||
Output "target/gauge.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Hide
|
||||
Type "cargo run --example=gauge --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 20s
|
||||
38
examples/generate.bash
Executable file
38
examples/generate.bash
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script is used to generate the images for the examples README
|
||||
# It requires the following tools:
|
||||
# - cargo: https://doc.rust-lang.org/cargo/getting-started/installation.html
|
||||
# - gh: https://github.com/cli/cli
|
||||
# - git: https://git-scm.com/
|
||||
# - vhs: https://github.com/charmbracelet/vhs
|
||||
|
||||
# Exit on error. Append "|| true" if you expect an error.
|
||||
set -o errexit
|
||||
# Exit on error inside any functions or subshells.
|
||||
set -o errtrace
|
||||
# Do not allow use of undefined vars. Use ${VAR:-} to use an undefined VAR
|
||||
set -o nounset
|
||||
# Catch the error in case mysqldump fails (but gzip succeeds) in `mysqldump |gzip`
|
||||
set -o pipefail
|
||||
# Turn on traces, useful while debugging but commented out by default
|
||||
# set -o xtrace
|
||||
|
||||
# ensure that running each example doesn't have to wait for the build
|
||||
cargo build --examples --features=crossterm,all-widgets
|
||||
|
||||
for tape in examples/*.tape
|
||||
do
|
||||
gif=${tape/examples\/}
|
||||
gif=${gif/.tape/.gif}
|
||||
vhs $tape --quiet
|
||||
# this can be pasted into the examples README.md
|
||||
echo "[${gif}]: https://github.com/ratatui-org/ratatui/blob/images/examples/${gif}?raw=true"
|
||||
done
|
||||
git switch images
|
||||
git pull --rebase upstream images
|
||||
cp target/*.gif examples/
|
||||
git add examples/*.gif
|
||||
git commit -m 'docs(examples): update images'
|
||||
gh pr create
|
||||
git sw main
|
||||
@@ -9,7 +9,7 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{backend::CrosstermBackend, widgets::Paragraph, Terminal};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
/// This is a bare minimum example. There are many approaches to running an application loop, so
|
||||
/// this is not meant to be prescriptive. It is only meant to demonstrate the basic setup and
|
||||
|
||||
12
examples/hello_world.tape
Normal file
12
examples/hello_world.tape
Normal file
@@ -0,0 +1,12 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/hello_world.tape`
|
||||
Output "target/hello_world.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 200
|
||||
Hide
|
||||
Type "cargo run --example=hello_world --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 5s
|
||||
@@ -8,15 +8,7 @@ use std::{
|
||||
};
|
||||
|
||||
use rand::distributions::{Distribution, Uniform};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
text::{Line, Span},
|
||||
widgets::{block::title::Title, Block, Gauge, LineGauge, List, ListItem, Paragraph, Widget},
|
||||
Frame, Terminal, TerminalOptions, Viewport,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
const NUM_DOWNLOADS: usize = 10;
|
||||
|
||||
@@ -227,7 +219,7 @@ fn run_app<B: Backend>(
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, downloads: &Downloads) {
|
||||
let size = f.size();
|
||||
|
||||
let block = Block::default().title(Title::from("Progress").alignment(Alignment::Center));
|
||||
let block = Block::default().title(block::Title::from("Progress").alignment(Alignment::Center));
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
|
||||
9
examples/inline.tape
Normal file
9
examples/inline.tape
Normal file
@@ -0,0 +1,9 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/inline.tape`
|
||||
Output "target/inline.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Type "cargo run --example=inline --features=crossterm"
|
||||
Enter
|
||||
Sleep 20s
|
||||
@@ -5,12 +5,8 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
widgets::{Block, Borders},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{layout::Constraint::*, prelude::*, widgets::*};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
@@ -51,21 +47,177 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>) {
|
||||
let chunks = Layout::default()
|
||||
fn ui<B: Backend>(frame: &mut Frame<B>) {
|
||||
let main_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(80),
|
||||
Constraint::Percentage(10),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
.constraints(vec![
|
||||
Length(4), // text
|
||||
Length(50), // examples
|
||||
Min(0), // fills remaining space
|
||||
])
|
||||
.split(frame.size());
|
||||
|
||||
let block = Block::default().title("Block").borders(Borders::ALL);
|
||||
f.render_widget(block, chunks[0]);
|
||||
let block = Block::default().title("Block 2").borders(Borders::ALL);
|
||||
f.render_widget(block, chunks[2]);
|
||||
// title
|
||||
frame.render_widget(
|
||||
Paragraph::new(vec![
|
||||
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("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"),
|
||||
]),
|
||||
main_layout[0],
|
||||
);
|
||||
|
||||
let example_rows = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![
|
||||
Length(9),
|
||||
Length(9),
|
||||
Length(9),
|
||||
Length(9),
|
||||
Length(9),
|
||||
Min(0), // fills remaining space
|
||||
])
|
||||
.split(main_layout[1]);
|
||||
let example_areas = example_rows
|
||||
.iter()
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![
|
||||
Length(14),
|
||||
Length(14),
|
||||
Length(14),
|
||||
Length(14),
|
||||
Length(14),
|
||||
Min(0), // fills remaining space
|
||||
])
|
||||
.split(*area)
|
||||
.iter()
|
||||
.copied()
|
||||
.take(5) // ignore Min(0)
|
||||
.collect_vec()
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
// the examples are a cartesian product of the following constraints
|
||||
// e.g. Len/Len, Len/Min, Len/Max, Len/Perc, Len/Ratio, Min/Len, Min/Min, ...
|
||||
let examples = [
|
||||
(
|
||||
"Len",
|
||||
vec![
|
||||
Length(0),
|
||||
Length(2),
|
||||
Length(3),
|
||||
Length(6),
|
||||
Length(10),
|
||||
Length(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",
|
||||
vec![
|
||||
Percentage(0),
|
||||
Percentage(25),
|
||||
Percentage(50),
|
||||
Percentage(75),
|
||||
Percentage(100),
|
||||
Percentage(150),
|
||||
],
|
||||
),
|
||||
(
|
||||
"Ratio",
|
||||
vec![
|
||||
Ratio(0, 4),
|
||||
Ratio(1, 4),
|
||||
Ratio(2, 4),
|
||||
Ratio(3, 4),
|
||||
Ratio(4, 4),
|
||||
Ratio(6, 4),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
for (i, (a, b)) in examples
|
||||
.iter()
|
||||
.cartesian_product(examples.iter())
|
||||
.enumerate()
|
||||
{
|
||||
let (name_a, examples_a) = a;
|
||||
let (name_b, examples_b) = b;
|
||||
let constraints = examples_a
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(examples_b.iter().copied())
|
||||
.collect_vec();
|
||||
render_example_combination(
|
||||
frame,
|
||||
example_areas[i],
|
||||
&format!("{name_a}/{name_b}"),
|
||||
constraints,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders a single example box
|
||||
fn render_example_combination<B: Backend>(
|
||||
frame: &mut Frame<B>,
|
||||
area: Rect,
|
||||
title: &str,
|
||||
constraints: Vec<(Constraint, Constraint)>,
|
||||
) {
|
||||
let block = Block::default()
|
||||
.title(title.gray())
|
||||
.style(Style::reset())
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::DarkGray));
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Length(1); constraints.len() + 1])
|
||||
.split(inner);
|
||||
for (i, (a, b)) in constraints.iter().enumerate() {
|
||||
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
|
||||
// with the constraints.
|
||||
frame.render_widget(Paragraph::new("123456789012"), layout[6]);
|
||||
}
|
||||
|
||||
/// Renders a single example line
|
||||
fn render_single_example<B: Backend>(
|
||||
frame: &mut Frame<B>,
|
||||
area: Rect,
|
||||
constraints: Vec<Constraint>,
|
||||
) {
|
||||
let red = Paragraph::new(constraint_label(constraints[0])).on_red();
|
||||
let blue = Paragraph::new(constraint_label(constraints[1])).on_blue();
|
||||
let green = Paragraph::new("·".repeat(12)).on_green();
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(constraints)
|
||||
.split(area);
|
||||
frame.render_widget(red, layout[0]);
|
||||
frame.render_widget(blue, layout[1]);
|
||||
frame.render_widget(green, layout[2]);
|
||||
}
|
||||
|
||||
fn constraint_label(constraint: Constraint) -> String {
|
||||
match constraint {
|
||||
Length(n) => format!("{n}"),
|
||||
Min(n) => format!("{n}"),
|
||||
Max(n) => format!("{n}"),
|
||||
Percentage(n) => format!("{n}"),
|
||||
Ratio(a, b) => format!("{a}:{b}"),
|
||||
}
|
||||
}
|
||||
|
||||
12
examples/layout.tape
Normal file
12
examples/layout.tape
Normal file
@@ -0,0 +1,12 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/layout.tape`
|
||||
Output "target/layout.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 1410
|
||||
Hide
|
||||
Type "cargo run --example=layout --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 2s
|
||||
@@ -9,14 +9,7 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Corner, Direction, Layout},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, List, ListItem, ListState},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
struct StatefulList<T> {
|
||||
state: ListState,
|
||||
|
||||
15
examples/list.tape
Normal file
15
examples/list.tape
Normal file
@@ -0,0 +1,15 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/list.tape`
|
||||
Output "target/list.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Hide
|
||||
Type "cargo run --example=list --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Down@1s 4
|
||||
Up@1s 2
|
||||
Left@1s 1
|
||||
Sleep 5s
|
||||
116
examples/modifiers.rs
Normal file
116
examples/modifiers.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
/// This example is useful for testing how your terminal emulator handles different modifiers.
|
||||
/// It will render a grid of combinations of foreground and background colors with all
|
||||
/// modifiers applied to them.
|
||||
use std::{
|
||||
error::Error,
|
||||
io::{self, Stdout},
|
||||
iter::once,
|
||||
result,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
type Result<T> = result::Result<T, Box<dyn Error>>;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut terminal = setup_terminal()?;
|
||||
let res = run_app(&mut terminal);
|
||||
restore_terminal(terminal)?;
|
||||
if let Err(err) = res {
|
||||
eprintln!("{err:?}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(ui)?;
|
||||
|
||||
if event::poll(Duration::from_millis(250))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(frame: &mut Frame<B>) {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Constraint::Length(1), Constraint::Min(0)])
|
||||
.split(frame.size());
|
||||
frame.render_widget(
|
||||
Paragraph::new("Note: not all terminals support all modifiers")
|
||||
.style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||
layout[0],
|
||||
);
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![Constraint::Length(1); 50])
|
||||
.split(layout[1])
|
||||
.iter()
|
||||
.flat_map(|area| {
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Percentage(20); 5])
|
||||
.split(*area)
|
||||
.to_vec()
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
let colors = [
|
||||
Color::Black,
|
||||
Color::DarkGray,
|
||||
Color::Gray,
|
||||
Color::White,
|
||||
Color::Red,
|
||||
];
|
||||
let all_modifiers = once(Modifier::empty())
|
||||
.chain(Modifier::all().iter())
|
||||
.collect_vec();
|
||||
let mut index = 0;
|
||||
for bg in colors.iter() {
|
||||
for fg in colors.iter() {
|
||||
for modifier in &all_modifiers {
|
||||
let modifier_name = format!("{modifier:11?}");
|
||||
let padding = (" ").repeat(12 - modifier_name.len());
|
||||
let paragraph = Paragraph::new(Line::from(vec![
|
||||
modifier_name.fg(*fg).bg(*bg).add_modifier(*modifier),
|
||||
padding.fg(*fg).bg(*bg).add_modifier(*modifier),
|
||||
// This is a hack to work around a bug in VHS which is used for rendering the
|
||||
// examples to gifs. The bug is that the background color of a paragraph seems
|
||||
// to bleed into the next character.
|
||||
".".black().on_black(),
|
||||
]));
|
||||
frame.render_widget(paragraph, layout[index]);
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
12
examples/modifiers.tape
Normal file
12
examples/modifiers.tape
Normal file
@@ -0,0 +1,12 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/modifiers.tape`
|
||||
Output "target/modifiers.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 1460
|
||||
Hide
|
||||
Type "cargo run --example=modifiers --features=crossterm"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 1s
|
||||
@@ -14,22 +14,13 @@
|
||||
//! That's why this example is set up to show both situations, with and without
|
||||
//! the chained panic hook, to see the difference.
|
||||
|
||||
#![deny(clippy::all)]
|
||||
#![warn(clippy::pedantic, clippy::nursery)]
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::Alignment,
|
||||
text::Line,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
|
||||
|
||||
20
examples/panic.tape
Normal file
20
examples/panic.tape
Normal file
@@ -0,0 +1,20 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/panic.tape`
|
||||
Output "target/panic.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Type "cargo run --example=panic --features=crossterm"
|
||||
Enter
|
||||
Sleep 5s
|
||||
Type p
|
||||
Sleep 2s
|
||||
Type reset
|
||||
Enter
|
||||
Type "cargo run --example=panic --features=crossterm"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Type e
|
||||
Sleep 2s
|
||||
Type p
|
||||
Sleep 5s
|
||||
@@ -9,14 +9,7 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Masked, Span},
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
struct App {
|
||||
scroll: u16,
|
||||
@@ -101,7 +94,6 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
|
||||
12
examples/paragraph.tape
Normal file
12
examples/paragraph.tape
Normal file
@@ -0,0 +1,12 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/paragraph.tape`
|
||||
Output "target/paragraph.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 1800
|
||||
Hide
|
||||
Type "cargo run --example=paragraph --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 5s
|
||||
@@ -5,13 +5,7 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::Stylize,
|
||||
widgets::{Block, Borders, Clear, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
struct App {
|
||||
show_popup: bool,
|
||||
|
||||
16
examples/popup.tape
Normal file
16
examples/popup.tape
Normal file
@@ -0,0 +1,16 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/popup.tape`
|
||||
Output "target/popup.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Hide
|
||||
Type "cargo run --example=popup --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 2s
|
||||
Type p
|
||||
Sleep 2s
|
||||
Type p
|
||||
Sleep 5s
|
||||
@@ -9,16 +9,7 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Alignment, Constraint, Direction, Layout, Margin},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Masked, Span},
|
||||
widgets::{
|
||||
scrollbar, Block, Borders, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState,
|
||||
},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use ratatui::{prelude::*, symbols::scrollbar, widgets::*};
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
@@ -75,27 +66,23 @@ fn run_app<B: Backend>(
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Char('j') => {
|
||||
app.vertical_scroll = app.vertical_scroll.saturating_add(1);
|
||||
app.vertical_scroll_state = app
|
||||
.vertical_scroll_state
|
||||
.position(app.vertical_scroll as u16);
|
||||
app.vertical_scroll_state =
|
||||
app.vertical_scroll_state.position(app.vertical_scroll);
|
||||
}
|
||||
KeyCode::Char('k') => {
|
||||
app.vertical_scroll = app.vertical_scroll.saturating_sub(1);
|
||||
app.vertical_scroll_state = app
|
||||
.vertical_scroll_state
|
||||
.position(app.vertical_scroll as u16);
|
||||
app.vertical_scroll_state =
|
||||
app.vertical_scroll_state.position(app.vertical_scroll);
|
||||
}
|
||||
KeyCode::Char('h') => {
|
||||
app.horizontal_scroll = app.horizontal_scroll.saturating_sub(1);
|
||||
app.horizontal_scroll_state = app
|
||||
.horizontal_scroll_state
|
||||
.position(app.horizontal_scroll as u16);
|
||||
app.horizontal_scroll_state =
|
||||
app.horizontal_scroll_state.position(app.horizontal_scroll);
|
||||
}
|
||||
KeyCode::Char('l') => {
|
||||
app.horizontal_scroll = app.horizontal_scroll.saturating_add(1);
|
||||
app.horizontal_scroll_state = app
|
||||
.horizontal_scroll_state
|
||||
.position(app.horizontal_scroll as u16);
|
||||
app.horizontal_scroll_state =
|
||||
app.horizontal_scroll_state.position(app.horizontal_scroll);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -120,7 +107,6 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Min(1),
|
||||
@@ -161,10 +147,8 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
),
|
||||
]),
|
||||
];
|
||||
app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len() as u16);
|
||||
app.horizontal_scroll_state = app
|
||||
.horizontal_scroll_state
|
||||
.content_length(long_line.len() as u16);
|
||||
app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len());
|
||||
app.horizontal_scroll_state = app.horizontal_scroll_state.content_length(long_line.len());
|
||||
|
||||
let create_block = |title| {
|
||||
Block::default()
|
||||
@@ -198,7 +182,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.gray()
|
||||
.block(create_block(
|
||||
"Vertical scrollbar without arrows and mirrored",
|
||||
"Vertical scrollbar without arrows, without track symbol and mirrored",
|
||||
))
|
||||
.scroll((app.vertical_scroll as u16, 0));
|
||||
f.render_widget(paragraph, chunks[2]);
|
||||
@@ -207,6 +191,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
.orientation(ScrollbarOrientation::VerticalLeft)
|
||||
.symbols(scrollbar::VERTICAL)
|
||||
.begin_symbol(None)
|
||||
.track_symbol(None)
|
||||
.end_symbol(None),
|
||||
chunks[2].inner(&Margin {
|
||||
vertical: 1,
|
||||
@@ -245,7 +230,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::HorizontalBottom)
|
||||
.thumb_symbol("░")
|
||||
.track_symbol("─"),
|
||||
.track_symbol(Some("─")),
|
||||
chunks[4].inner(&Margin {
|
||||
vertical: 0,
|
||||
horizontal: 1,
|
||||
|
||||
12
examples/scrollbar.tape
Normal file
12
examples/scrollbar.tape
Normal file
@@ -0,0 +1,12 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/scrollbar.tape`
|
||||
Output "target/scrollbar.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 1200
|
||||
Hide
|
||||
Type "cargo run --example=scrollbar --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 5s
|
||||
@@ -13,13 +13,7 @@ use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Borders, Sparkline},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RandomSignal {
|
||||
@@ -135,12 +129,10 @@ fn run_app<B: Backend>(
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(7),
|
||||
Constraint::Min(0),
|
||||
]
|
||||
.as_ref(),
|
||||
|
||||
12
examples/sparkline.tape
Normal file
12
examples/sparkline.tape
Normal file
@@ -0,0 +1,12 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/sparkline.tape`
|
||||
Output "target/sparkline.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Hide
|
||||
Type "cargo run --example=sparkline --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 5s
|
||||
@@ -5,13 +5,7 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{Block, Borders, Cell, Row, Table, TableState},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
struct App<'a> {
|
||||
state: TableState,
|
||||
@@ -122,7 +116,6 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
let rects = Layout::default()
|
||||
.constraints([Constraint::Percentage(100)].as_ref())
|
||||
.margin(5)
|
||||
.split(f.size());
|
||||
|
||||
let selected_style = Style::default().add_modifier(Modifier::REVERSED);
|
||||
@@ -151,7 +144,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
.highlight_symbol(">> ")
|
||||
.widths(&[
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Length(30),
|
||||
Constraint::Max(30),
|
||||
Constraint::Min(10),
|
||||
]);
|
||||
f.render_stateful_widget(t, rects[0], &mut app.state);
|
||||
|
||||
16
examples/table.tape
Normal file
16
examples/table.tape
Normal file
@@ -0,0 +1,16 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/table.tape`
|
||||
Output "target/table.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Hide
|
||||
Type "cargo run --example=table --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Down@1s 4
|
||||
Up@1s 2
|
||||
Down@1s 8
|
||||
Up@1s 12
|
||||
Sleep 5s
|
||||
@@ -5,14 +5,7 @@ use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::Line,
|
||||
widgets::{Block, Borders, Tabs},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
struct App<'a> {
|
||||
pub titles: Vec<&'a str>,
|
||||
@@ -89,7 +82,6 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let size = f.size();
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(5)
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
||||
.split(size);
|
||||
|
||||
|
||||
14
examples/tabs.tape
Normal file
14
examples/tabs.tape
Normal file
@@ -0,0 +1,14 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/tabs.tape`
|
||||
Output "target/tabs.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 300
|
||||
Hide
|
||||
Type "cargo run --example=tabs --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Right@1s 4
|
||||
Left@1s 2
|
||||
Sleep 5s
|
||||
@@ -6,25 +6,20 @@ use std::{error::Error, io};
|
||||
/// started.
|
||||
///
|
||||
/// This is a very simple example:
|
||||
/// * A input box always focused. Every character you type is registered
|
||||
/// here
|
||||
/// * Pressing Backspace erases a character
|
||||
/// * An input box always focused. Every character you type is registered
|
||||
/// here.
|
||||
/// * An entered character is inserted at the cursor position.
|
||||
/// * Pressing Backspace erases the left character before the cursor position
|
||||
/// * Pressing Enter pushes the current input in the history of previous
|
||||
/// messages
|
||||
/// messages.
|
||||
/// **Note: ** as this is a relatively simple example unicode characters are unsupported and
|
||||
/// their use will result in undefined behaviour.
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, Borders, List, ListItem, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
enum InputMode {
|
||||
Normal,
|
||||
@@ -35,6 +30,8 @@ enum InputMode {
|
||||
struct App {
|
||||
/// Current value of the input box
|
||||
input: String,
|
||||
/// Position of cursor in the editor area.
|
||||
cursor_position: usize,
|
||||
/// Current input mode
|
||||
input_mode: InputMode,
|
||||
/// History of recorded messages
|
||||
@@ -47,10 +44,65 @@ impl Default for App {
|
||||
input: String::new(),
|
||||
input_mode: InputMode::Normal,
|
||||
messages: Vec::new(),
|
||||
cursor_position: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn move_cursor_left(&mut self) {
|
||||
let cursor_moved_left = self.cursor_position.saturating_sub(1);
|
||||
self.cursor_position = self.clamp_cursor(cursor_moved_left);
|
||||
}
|
||||
|
||||
fn move_cursor_right(&mut self) {
|
||||
let cursor_moved_right = self.cursor_position.saturating_add(1);
|
||||
self.cursor_position = self.clamp_cursor(cursor_moved_right);
|
||||
}
|
||||
|
||||
fn enter_char(&mut self, new_char: char) {
|
||||
self.input.insert(self.cursor_position, new_char);
|
||||
|
||||
self.move_cursor_right();
|
||||
}
|
||||
|
||||
fn delete_char(&mut self) {
|
||||
let is_not_cursor_leftmost = self.cursor_position != 0;
|
||||
if is_not_cursor_leftmost {
|
||||
// Method "remove" is not used on the saved text for deleting the selected char.
|
||||
// Reason: Using remove on String works on bytes instead of the chars.
|
||||
// Using remove would require special care because of char boundaries.
|
||||
|
||||
let current_index = self.cursor_position;
|
||||
let from_left_to_current_index = current_index - 1;
|
||||
|
||||
// Getting all characters before the selected character.
|
||||
let before_char_to_delete = self.input.chars().take(from_left_to_current_index);
|
||||
// Getting all characters after selected character.
|
||||
let after_char_to_delete = self.input.chars().skip(current_index);
|
||||
|
||||
// Put all characters together except the selected one.
|
||||
// By leaving the selected one out, it is forgotten and therefore deleted.
|
||||
self.input = before_char_to_delete.chain(after_char_to_delete).collect();
|
||||
self.move_cursor_left();
|
||||
}
|
||||
}
|
||||
|
||||
fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
|
||||
new_cursor_pos.clamp(0, self.input.len())
|
||||
}
|
||||
|
||||
fn reset_cursor(&mut self) {
|
||||
self.cursor_position = 0;
|
||||
}
|
||||
|
||||
fn submit_message(&mut self) {
|
||||
self.messages.push(self.input.clone());
|
||||
self.input.clear();
|
||||
self.reset_cursor();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
@@ -95,14 +147,18 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
_ => {}
|
||||
},
|
||||
InputMode::Editing if key.kind == KeyEventKind::Press => match key.code {
|
||||
KeyCode::Enter => {
|
||||
app.messages.push(app.input.drain(..).collect());
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
app.input.push(c);
|
||||
KeyCode::Enter => app.submit_message(),
|
||||
KeyCode::Char(to_insert) => {
|
||||
app.enter_char(to_insert);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
app.input.pop();
|
||||
app.delete_char();
|
||||
}
|
||||
KeyCode::Left => {
|
||||
app.move_cursor_left();
|
||||
}
|
||||
KeyCode::Right => {
|
||||
app.move_cursor_right();
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
app.input_mode = InputMode::Normal;
|
||||
@@ -118,7 +174,6 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(1),
|
||||
@@ -134,7 +189,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
vec![
|
||||
"Press ".into(),
|
||||
"q".bold(),
|
||||
" to exist, ".into(),
|
||||
" to exit, ".into(),
|
||||
"e".bold(),
|
||||
" to start editing.".bold(),
|
||||
],
|
||||
@@ -172,8 +227,9 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
// Make the cursor visible and ask ratatui to put it at the specified coordinates after
|
||||
// rendering
|
||||
f.set_cursor(
|
||||
// Put cursor past the end of the input text
|
||||
chunks[1].x + app.input.width() as u16 + 1,
|
||||
// Draw the cursor at the current position in the input field.
|
||||
// This position is can be controlled via the left and right arrow key
|
||||
chunks[1].x + app.cursor_position as u16 + 1,
|
||||
// Move one line down, from the border to the input line
|
||||
chunks[1].y + 1,
|
||||
)
|
||||
|
||||
22
examples/user_input.tape
Normal file
22
examples/user_input.tape
Normal file
@@ -0,0 +1,22 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/user_input.tape`
|
||||
Output "target/user_input.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Hide
|
||||
Type "cargo run --example=user_input --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 2s
|
||||
Type e
|
||||
Sleep 1s
|
||||
Type "Hello, world!"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Backspace 13
|
||||
Sleep 1s
|
||||
Type "Goodbye, world!"
|
||||
Enter
|
||||
Sleep 5s
|
||||
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "stable"
|
||||
@@ -12,15 +12,16 @@ use crossterm::{
|
||||
execute, queue,
|
||||
style::{
|
||||
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
|
||||
SetForegroundColor,
|
||||
SetForegroundColor, SetUnderlineColor,
|
||||
},
|
||||
terminal::{self, Clear},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, ClearType},
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
layout::Size,
|
||||
prelude::Rect,
|
||||
style::{Color, Modifier},
|
||||
};
|
||||
|
||||
@@ -42,6 +43,7 @@ use crate::{
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct CrosstermBackend<W: Write> {
|
||||
buffer: W,
|
||||
}
|
||||
@@ -81,12 +83,13 @@ where
|
||||
{
|
||||
let mut fg = Color::Reset;
|
||||
let mut bg = Color::Reset;
|
||||
let mut underline_color = Color::Reset;
|
||||
let mut modifier = Modifier::empty();
|
||||
let mut last_pos: Option<(u16, u16)> = None;
|
||||
for (x, y, cell) in content {
|
||||
// Move the cursor if the previous location was not (x - 1, y)
|
||||
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
|
||||
map_error(queue!(self.buffer, MoveTo(x, y)))?;
|
||||
queue!(self.buffer, MoveTo(x, y))?;
|
||||
}
|
||||
last_pos = Some((x, y));
|
||||
if cell.modifier != modifier {
|
||||
@@ -99,32 +102,38 @@ where
|
||||
}
|
||||
if cell.fg != fg {
|
||||
let color = CColor::from(cell.fg);
|
||||
map_error(queue!(self.buffer, SetForegroundColor(color)))?;
|
||||
queue!(self.buffer, SetForegroundColor(color))?;
|
||||
fg = cell.fg;
|
||||
}
|
||||
if cell.bg != bg {
|
||||
let color = CColor::from(cell.bg);
|
||||
map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
|
||||
queue!(self.buffer, SetBackgroundColor(color))?;
|
||||
bg = cell.bg;
|
||||
}
|
||||
if cell.underline_color != underline_color {
|
||||
let color = CColor::from(cell.underline_color);
|
||||
queue!(self.buffer, SetUnderlineColor(color))?;
|
||||
underline_color = cell.underline_color;
|
||||
}
|
||||
|
||||
map_error(queue!(self.buffer, Print(&cell.symbol)))?;
|
||||
queue!(self.buffer, Print(&cell.symbol))?;
|
||||
}
|
||||
|
||||
map_error(queue!(
|
||||
queue!(
|
||||
self.buffer,
|
||||
SetForegroundColor(CColor::Reset),
|
||||
SetBackgroundColor(CColor::Reset),
|
||||
SetUnderlineColor(CColor::Reset),
|
||||
SetAttribute(CAttribute::Reset)
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
map_error(execute!(self.buffer, Hide))
|
||||
execute!(self.buffer, Hide)
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
map_error(execute!(self.buffer, Show))
|
||||
execute!(self.buffer, Show)
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
@@ -133,7 +142,7 @@ where
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
map_error(execute!(self.buffer, MoveTo(x, y)))
|
||||
execute!(self.buffer, MoveTo(x, y))
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
@@ -141,7 +150,7 @@ where
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
map_error(execute!(
|
||||
execute!(
|
||||
self.buffer,
|
||||
Clear(match clear_type {
|
||||
ClearType::All => crossterm::terminal::ClearType::All,
|
||||
@@ -150,32 +159,42 @@ where
|
||||
ClearType::CurrentLine => crossterm::terminal::ClearType::CurrentLine,
|
||||
ClearType::UntilNewLine => crossterm::terminal::ClearType::UntilNewLine,
|
||||
})
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||
for _ in 0..n {
|
||||
map_error(queue!(self.buffer, Print("\n")))?;
|
||||
queue!(self.buffer, Print("\n"))?;
|
||||
}
|
||||
self.buffer.flush()
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
let (width, height) =
|
||||
terminal::size().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
||||
|
||||
let (width, height) = terminal::size()?;
|
||||
Ok(Rect::new(0, 0, width, height))
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
|
||||
let crossterm::terminal::WindowSize {
|
||||
columns,
|
||||
rows,
|
||||
width,
|
||||
height,
|
||||
} = terminal::window_size()?;
|
||||
Ok(WindowSize {
|
||||
columns_rows: Size {
|
||||
width: columns,
|
||||
height: rows,
|
||||
},
|
||||
pixels: Size { width, height },
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.buffer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
fn map_error(error: crossterm::Result<()>) -> io::Result<()> {
|
||||
error.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||
}
|
||||
|
||||
impl From<Color> for CColor {
|
||||
fn from(color: Color) -> Self {
|
||||
match color {
|
||||
@@ -205,7 +224,7 @@ impl From<Color> for CColor {
|
||||
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
|
||||
/// values. This is useful when updating the terminal display, as it allows for more
|
||||
/// efficient updates by only sending the necessary changes.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
struct ModifierDiff {
|
||||
pub from: Modifier,
|
||||
pub to: Modifier,
|
||||
@@ -219,54 +238,54 @@ impl ModifierDiff {
|
||||
//use crossterm::Attribute;
|
||||
let removed = self.from - self.to;
|
||||
if removed.contains(Modifier::REVERSED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?;
|
||||
queue!(w, SetAttribute(CAttribute::NoReverse))?;
|
||||
}
|
||||
if removed.contains(Modifier::BOLD) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
|
||||
queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
|
||||
if self.to.contains(Modifier::DIM) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
|
||||
queue!(w, SetAttribute(CAttribute::Dim))?;
|
||||
}
|
||||
}
|
||||
if removed.contains(Modifier::ITALIC) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
|
||||
queue!(w, SetAttribute(CAttribute::NoItalic))?;
|
||||
}
|
||||
if removed.contains(Modifier::UNDERLINED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
|
||||
queue!(w, SetAttribute(CAttribute::NoUnderline))?;
|
||||
}
|
||||
if removed.contains(Modifier::DIM) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
|
||||
queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
|
||||
}
|
||||
if removed.contains(Modifier::CROSSED_OUT) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NotCrossedOut)))?;
|
||||
queue!(w, SetAttribute(CAttribute::NotCrossedOut))?;
|
||||
}
|
||||
if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?;
|
||||
queue!(w, SetAttribute(CAttribute::NoBlink))?;
|
||||
}
|
||||
|
||||
let added = self.to - self.from;
|
||||
if added.contains(Modifier::REVERSED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?;
|
||||
queue!(w, SetAttribute(CAttribute::Reverse))?;
|
||||
}
|
||||
if added.contains(Modifier::BOLD) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Bold)))?;
|
||||
queue!(w, SetAttribute(CAttribute::Bold))?;
|
||||
}
|
||||
if added.contains(Modifier::ITALIC) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Italic)))?;
|
||||
queue!(w, SetAttribute(CAttribute::Italic))?;
|
||||
}
|
||||
if added.contains(Modifier::UNDERLINED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
|
||||
queue!(w, SetAttribute(CAttribute::Underlined))?;
|
||||
}
|
||||
if added.contains(Modifier::DIM) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
|
||||
queue!(w, SetAttribute(CAttribute::Dim))?;
|
||||
}
|
||||
if added.contains(Modifier::CROSSED_OUT) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::CrossedOut)))?;
|
||||
queue!(w, SetAttribute(CAttribute::CrossedOut))?;
|
||||
}
|
||||
if added.contains(Modifier::SLOW_BLINK) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::SlowBlink)))?;
|
||||
queue!(w, SetAttribute(CAttribute::SlowBlink))?;
|
||||
}
|
||||
if added.contains(Modifier::RAPID_BLINK) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::RapidBlink)))?;
|
||||
queue!(w, SetAttribute(CAttribute::RapidBlink))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -27,7 +27,9 @@
|
||||
|
||||
use std::io;
|
||||
|
||||
use crate::{buffer::Cell, layout::Rect};
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::{buffer::Cell, layout::Size, prelude::Rect};
|
||||
|
||||
#[cfg(feature = "termion")]
|
||||
mod termion;
|
||||
@@ -49,7 +51,7 @@ pub use self::test::TestBackend;
|
||||
|
||||
/// Enum representing the different types of clearing operations that can be performed
|
||||
/// on the terminal screen.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum ClearType {
|
||||
All,
|
||||
AfterCursor,
|
||||
@@ -58,6 +60,18 @@ pub enum ClearType {
|
||||
UntilNewLine,
|
||||
}
|
||||
|
||||
/// The window sizes in columns,rows and optionally pixel width,height.
|
||||
pub struct WindowSize {
|
||||
/// Size in character/cell columents,rows.
|
||||
pub columns_rows: Size,
|
||||
/// Size in pixel width,height.
|
||||
///
|
||||
/// The `pixels` fields may not be implemented by all terminals and return `0,0`.
|
||||
/// See <https://man7.org/linux/man-pages/man4/tty_ioctl.4.html> under section
|
||||
/// "Get and set window size" / TIOCGWINSZ where the fields are commented as "unused".
|
||||
pub pixels: Size,
|
||||
}
|
||||
|
||||
/// The `Backend` trait provides an abstraction over different terminal libraries.
|
||||
/// It defines the methods required to draw content, manipulate the cursor, and
|
||||
/// clear the terminal screen.
|
||||
@@ -109,9 +123,54 @@ pub trait Backend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the size of the terminal screen as a [`Rect`].
|
||||
/// Get the size of the terminal screen in columns/rows as a [`Rect`].
|
||||
fn size(&self) -> Result<Rect, io::Error>;
|
||||
|
||||
/// Get the size of the terminal screen in columns/rows and pixels as [`WindowSize`].
|
||||
///
|
||||
/// The reason for this not returning only the pixel size, given the redundancy with the
|
||||
/// `size()` method, is that the underlying backends most likely get both values with one
|
||||
/// syscall, and the user is also most likely to need columns,rows together with pixel size.
|
||||
fn window_size(&mut self) -> Result<WindowSize, io::Error>;
|
||||
|
||||
/// Flush any buffered content to the terminal screen.
|
||||
fn flush(&mut self) -> Result<(), io::Error>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::ParseError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn clear_type_tostring() {
|
||||
assert_eq!(ClearType::All.to_string(), "All");
|
||||
assert_eq!(ClearType::AfterCursor.to_string(), "AfterCursor");
|
||||
assert_eq!(ClearType::BeforeCursor.to_string(), "BeforeCursor");
|
||||
assert_eq!(ClearType::CurrentLine.to_string(), "CurrentLine");
|
||||
assert_eq!(ClearType::UntilNewLine.to_string(), "UntilNewLine");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_type_from_str() {
|
||||
assert_eq!("All".parse::<ClearType>(), Ok(ClearType::All));
|
||||
assert_eq!(
|
||||
"AfterCursor".parse::<ClearType>(),
|
||||
Ok(ClearType::AfterCursor)
|
||||
);
|
||||
assert_eq!(
|
||||
"BeforeCursor".parse::<ClearType>(),
|
||||
Ok(ClearType::BeforeCursor)
|
||||
);
|
||||
assert_eq!(
|
||||
"CurrentLine".parse::<ClearType>(),
|
||||
Ok(ClearType::CurrentLine)
|
||||
);
|
||||
assert_eq!(
|
||||
"UntilNewLine".parse::<ClearType>(),
|
||||
Ok(ClearType::UntilNewLine)
|
||||
);
|
||||
assert_eq!("".parse::<ClearType>(), Err(ParseError::VariantNotFound));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, ClearType},
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
prelude::Rect,
|
||||
style::{Color, Modifier},
|
||||
};
|
||||
|
||||
@@ -31,6 +31,7 @@ use crate::{
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct TermionBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
@@ -159,18 +160,27 @@ where
|
||||
Ok(Rect::new(0, 0, terminal.0, terminal.1))
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
|
||||
Ok(WindowSize {
|
||||
columns_rows: termion::terminal_size()?.into(),
|
||||
pixels: termion::terminal_size_pixels()?.into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.stdout.flush()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
struct Fg(Color);
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
struct Bg(Color);
|
||||
|
||||
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
|
||||
/// values. This is useful when updating the terminal display, as it allows for more
|
||||
/// efficient updates by only sending the necessary changes.
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
struct ModifierDiff {
|
||||
from: Modifier,
|
||||
to: Modifier,
|
||||
|
||||
@@ -11,20 +11,21 @@ use termwiz::{
|
||||
cell::{AttributeChange, Blink, Intensity, Underline},
|
||||
color::{AnsiColor, ColorAttribute, SrgbaTuple},
|
||||
surface::{Change, CursorVisibility, Position},
|
||||
terminal::{buffered::BufferedTerminal, SystemTerminal, Terminal},
|
||||
terminal::{buffered::BufferedTerminal, ScreenSize, SystemTerminal, Terminal},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::Backend,
|
||||
backend::{Backend, WindowSize},
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
layout::Size,
|
||||
prelude::Rect,
|
||||
style::{Color, Modifier},
|
||||
};
|
||||
|
||||
/// Termwiz backend implementation for the [`Backend`] trait.
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// ```rust,no_run
|
||||
/// use ratatui::backend::{Backend, TermwizBackend};
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
@@ -169,22 +170,31 @@ impl Backend for TermwizBackend {
|
||||
}
|
||||
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
let (term_width, term_height) = self.buffered_terminal.dimensions();
|
||||
let max = u16::max_value();
|
||||
Ok(Rect::new(
|
||||
0,
|
||||
0,
|
||||
if term_width > usize::from(max) {
|
||||
max
|
||||
} else {
|
||||
term_width as u16
|
||||
let (cols, rows) = self.buffered_terminal.dimensions();
|
||||
Ok(Rect::new(0, 0, u16_max(cols), u16_max(rows)))
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
|
||||
let ScreenSize {
|
||||
cols,
|
||||
rows,
|
||||
xpixel,
|
||||
ypixel,
|
||||
} = self
|
||||
.buffered_terminal
|
||||
.terminal()
|
||||
.get_screen_size()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
Ok(WindowSize {
|
||||
columns_rows: Size {
|
||||
width: u16_max(cols),
|
||||
height: u16_max(rows),
|
||||
},
|
||||
if term_height > usize::from(max) {
|
||||
max
|
||||
} else {
|
||||
term_height as u16
|
||||
pixels: Size {
|
||||
width: u16_max(xpixel),
|
||||
height: u16_max(ypixel),
|
||||
},
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
@@ -221,3 +231,8 @@ impl From<Color> for ColorAttribute {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn u16_max(i: usize) -> u16 {
|
||||
u16::try_from(i).unwrap_or(u16::MAX)
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ use std::{
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
backend::Backend,
|
||||
backend::{Backend, WindowSize},
|
||||
buffer::{Buffer, Cell},
|
||||
layout::Rect,
|
||||
layout::{Rect, Size},
|
||||
};
|
||||
|
||||
/// A backend used for the integration tests.
|
||||
@@ -28,7 +28,8 @@ use crate::{
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct TestBackend {
|
||||
width: u16,
|
||||
buffer: Buffer,
|
||||
@@ -93,6 +94,7 @@ impl TestBackend {
|
||||
/// Asserts that the TestBackend's buffer is equal to the expected buffer.
|
||||
/// If the buffers are not equal, a panic occurs with a detailed error message
|
||||
/// showing the differences between the expected and actual buffers.
|
||||
#[track_caller]
|
||||
pub fn assert_buffer(&self, expected: &Buffer) {
|
||||
assert_eq!(expected.area, self.buffer.area);
|
||||
let diff = expected.diff(&self.buffer);
|
||||
@@ -177,7 +179,151 @@ impl Backend for TestBackend {
|
||||
Ok(Rect::new(0, 0, self.width, self.height))
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> Result<WindowSize, io::Error> {
|
||||
// Some arbitrary window pixel size, probably doesn't need much testing.
|
||||
static WINDOW_PIXEL_SIZE: Size = Size {
|
||||
width: 640,
|
||||
height: 480,
|
||||
};
|
||||
Ok(WindowSize {
|
||||
columns_rows: (self.width, self.height).into(),
|
||||
pixels: WINDOW_PIXEL_SIZE,
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
assert_eq!(
|
||||
TestBackend::new(10, 2),
|
||||
TestBackend {
|
||||
width: 10,
|
||||
height: 2,
|
||||
buffer: Buffer::with_lines(vec![" "; 2]),
|
||||
cursor: false,
|
||||
pos: (0, 0),
|
||||
}
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_buffer_view() {
|
||||
let buffer = Buffer::with_lines(vec!["aaaa"; 2]);
|
||||
assert_eq!(buffer_view(&buffer), "\"aaaa\"\n\"aaaa\"\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_view_with_overwrites() {
|
||||
let multi_byte_char = "👨👩👧👦"; // renders 8 wide
|
||||
let buffer = Buffer::with_lines(vec![multi_byte_char]);
|
||||
assert_eq!(
|
||||
buffer_view(&buffer),
|
||||
format!(
|
||||
r#""{multi_byte_char}" Hidden by multi-width symbols: [(1, " "), (2, " "), (3, " "), (4, " "), (5, " "), (6, " "), (7, " ")]
|
||||
"#,
|
||||
multi_byte_char = multi_byte_char
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer() {
|
||||
let backend = TestBackend::new(10, 2);
|
||||
assert_eq!(backend.buffer(), &Buffer::with_lines(vec![" "; 2]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize() {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
backend.resize(5, 5);
|
||||
assert_eq!(backend.buffer(), &Buffer::with_lines(vec![" "; 5]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assert_buffer() {
|
||||
let backend = TestBackend::new(10, 2);
|
||||
let buffer = Buffer::with_lines(vec![" "; 2]);
|
||||
backend.assert_buffer(&buffer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn assert_buffer_panics() {
|
||||
let backend = TestBackend::new(10, 2);
|
||||
let buffer = Buffer::with_lines(vec!["aaaaaaaaaa"; 2]);
|
||||
backend.assert_buffer(&buffer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display() {
|
||||
let backend = TestBackend::new(10, 2);
|
||||
assert_eq!(format!("{}", backend), "\" \"\n\" \"\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draw() {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
let mut cell = Cell::default();
|
||||
cell.set_symbol("a");
|
||||
backend.draw([(0, 0, &cell)].into_iter()).unwrap();
|
||||
backend.draw([(0, 1, &cell)].into_iter()).unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec!["a "; 2]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hide_cursor() {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
backend.hide_cursor().unwrap();
|
||||
assert!(!backend.cursor);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_cursor() {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
backend.show_cursor().unwrap();
|
||||
assert!(backend.cursor);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_cursor() {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
assert_eq!(backend.get_cursor().unwrap(), (0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_cursor() {
|
||||
let mut backend = TestBackend::new(10, 10);
|
||||
backend.set_cursor(5, 5).unwrap();
|
||||
assert_eq!(backend.pos, (5, 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear() {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
let mut cell = Cell::default();
|
||||
cell.set_symbol("a");
|
||||
backend.draw([(0, 0, &cell)].into_iter()).unwrap();
|
||||
backend.draw([(0, 1, &cell)].into_iter()).unwrap();
|
||||
backend.clear().unwrap();
|
||||
backend.assert_buffer(&Buffer::with_lines(vec![" "; 2]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size() {
|
||||
let backend = TestBackend::new(10, 2);
|
||||
assert_eq!(backend.size().unwrap(), Rect::new(0, 0, 10, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flush() {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
backend.flush().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
155
src/buffer.rs
155
src/buffer.rs
@@ -14,12 +14,16 @@ use crate::{
|
||||
};
|
||||
|
||||
/// A buffer cell
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Cell {
|
||||
pub symbol: String,
|
||||
pub fg: Color,
|
||||
pub bg: Color,
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub underline_color: Color,
|
||||
pub modifier: Modifier,
|
||||
pub skip: bool,
|
||||
}
|
||||
|
||||
impl Cell {
|
||||
@@ -52,11 +56,25 @@ impl Cell {
|
||||
if let Some(c) = style.bg {
|
||||
self.bg = c;
|
||||
}
|
||||
#[cfg(feature = "crossterm")]
|
||||
if let Some(c) = style.underline_color {
|
||||
self.underline_color = c;
|
||||
}
|
||||
self.modifier.insert(style.add_modifier);
|
||||
self.modifier.remove(style.sub_modifier);
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub fn style(&self) -> Style {
|
||||
Style::default()
|
||||
.fg(self.fg)
|
||||
.bg(self.bg)
|
||||
.underline_color(self.underline_color)
|
||||
.add_modifier(self.modifier)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "crossterm"))]
|
||||
pub fn style(&self) -> Style {
|
||||
Style::default()
|
||||
.fg(self.fg)
|
||||
@@ -64,12 +82,26 @@ impl Cell {
|
||||
.add_modifier(self.modifier)
|
||||
}
|
||||
|
||||
/// Sets the cell to be skipped when copying (diffing) the buffer to the screen.
|
||||
///
|
||||
/// This is helpful when it is necessary to prevent the buffer from overwriting a cell that is
|
||||
/// covered by an image from some terminal graphics protocol (Sixel / iTerm / Kitty ...).
|
||||
pub fn set_skip(&mut self, skip: bool) -> &mut Cell {
|
||||
self.skip = skip;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.symbol.clear();
|
||||
self.symbol.push(' ');
|
||||
self.fg = Color::Reset;
|
||||
self.bg = Color::Reset;
|
||||
#[cfg(feature = "crossterm")]
|
||||
{
|
||||
self.underline_color = Color::Reset;
|
||||
}
|
||||
self.modifier = Modifier::empty();
|
||||
self.skip = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +111,10 @@ impl Default for Cell {
|
||||
symbol: " ".into(),
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
#[cfg(feature = "crossterm")]
|
||||
underline_color: Color::Reset,
|
||||
modifier: Modifier::empty(),
|
||||
skip: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,12 +141,16 @@ impl Default for Cell {
|
||||
/// symbol: String::from("r"),
|
||||
/// fg: Color::Red,
|
||||
/// bg: Color::White,
|
||||
/// modifier: Modifier::empty()
|
||||
/// #[cfg(feature = "crossterm")]
|
||||
/// underline_color: Color::Reset,
|
||||
/// modifier: Modifier::empty(),
|
||||
/// skip: false
|
||||
/// });
|
||||
/// buf.get_mut(5, 0).set_char('x');
|
||||
/// assert_eq!(buf.get(5, 0).symbol, "x");
|
||||
/// ```
|
||||
#[derive(Clone, PartialEq, Eq, Default)]
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Buffer {
|
||||
/// The area represented by this buffer
|
||||
pub area: Rect,
|
||||
@@ -462,10 +501,10 @@ impl Buffer {
|
||||
// Cells invalidated by drawing/replacing preceding multi-width characters:
|
||||
let mut invalidated: usize = 0;
|
||||
// Cells from the current buffer to skip due to preceding multi-width characters taking
|
||||
// their place (the skipped cells should be blank anyway):
|
||||
// their place (the skipped cells should be blank anyway), or due to per-cell-skipping:
|
||||
let mut to_skip: usize = 0;
|
||||
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
|
||||
if (current != previous || invalidated > 0) && to_skip == 0 {
|
||||
if !current.skip && (current != previous || invalidated > 0) && to_skip == 0 {
|
||||
let (x, y) = self.pos_of(i);
|
||||
updates.push((x, y, &next_buffer[i]));
|
||||
}
|
||||
@@ -559,10 +598,21 @@ impl Debug for Buffer {
|
||||
overwritten.push((x, &c.symbol));
|
||||
}
|
||||
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
|
||||
let style = (c.fg, c.bg, c.modifier);
|
||||
if last_style != Some(style) {
|
||||
last_style = Some(style);
|
||||
styles.push((x, y, c.fg, c.bg, c.modifier));
|
||||
#[cfg(feature = "crossterm")]
|
||||
{
|
||||
let style = (c.fg, c.bg, c.underline_color, c.modifier);
|
||||
if last_style != Some(style) {
|
||||
last_style = Some(style);
|
||||
styles.push((x, y, c.fg, c.bg, c.underline_color, c.modifier));
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "crossterm"))]
|
||||
{
|
||||
let style = (c.fg, c.bg, c.modifier);
|
||||
if last_style != Some(style) {
|
||||
last_style = Some(style);
|
||||
styles.push((x, y, c.fg, c.bg, c.modifier));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !overwritten.is_empty() {
|
||||
@@ -574,6 +624,12 @@ impl Debug for Buffer {
|
||||
}
|
||||
f.write_str(" ],\n styles: [\n")?;
|
||||
for s in styles {
|
||||
#[cfg(feature = "crossterm")]
|
||||
f.write_fmt(format_args!(
|
||||
" x: {}, y: {}, fg: {:?}, bg: {:?}, underline: {:?}, modifier: {:?},\n",
|
||||
s.0, s.1, s.2, s.3, s.4, s.5
|
||||
))?;
|
||||
#[cfg(not(feature = "crossterm"))]
|
||||
f.write_fmt(format_args!(
|
||||
" x: {}, y: {}, fg: {:?}, bg: {:?}, modifier: {:?},\n",
|
||||
s.0, s.1, s.2, s.3, s.4
|
||||
@@ -607,6 +663,25 @@ mod tests {
|
||||
.bg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
#[cfg(feature = "crossterm")]
|
||||
assert_eq!(
|
||||
format!("{buf:?}"),
|
||||
indoc::indoc!(
|
||||
"
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 12, height: 2 },
|
||||
content: [
|
||||
\"Hello World!\",
|
||||
\"G'day World!\",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 1, fg: Green, bg: Yellow, underline: Reset, modifier: BOLD,
|
||||
]
|
||||
}"
|
||||
)
|
||||
);
|
||||
#[cfg(not(feature = "crossterm"))]
|
||||
assert_eq!(
|
||||
format!("{buf:?}"),
|
||||
indoc::indoc!(
|
||||
@@ -854,6 +929,18 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_diffing_skip() {
|
||||
let prev = Buffer::with_lines(vec!["123"]);
|
||||
let mut next = Buffer::with_lines(vec!["456"]);
|
||||
for i in 1..3 {
|
||||
next.content[i].set_skip(true);
|
||||
}
|
||||
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![(0, 0, &cell("4"))],);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_merge() {
|
||||
let mut one = Buffer::filled(
|
||||
@@ -935,4 +1022,54 @@ mod tests {
|
||||
};
|
||||
assert_buffer_eq!(one, merged);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_merge_skip() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2").set_skip(true),
|
||||
);
|
||||
one.merge(&two);
|
||||
let skipped: Vec<bool> = one.content().iter().map(|c| c.skip).collect();
|
||||
assert_eq!(skipped, vec![false, false, true, true, true, true]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_merge_skip2() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1").set_skip(true),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 1,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
let skipped: Vec<bool> = one.content().iter().map(|c| c.skip).collect();
|
||||
assert_eq!(skipped, vec![true, true, false, false, false, false]);
|
||||
}
|
||||
}
|
||||
|
||||
1556
src/layout.rs
1556
src/layout.rs
File diff suppressed because it is too large
Load Diff
52
src/lib.rs
52
src/lib.rs
@@ -1,7 +1,9 @@
|
||||
//! [ratatui](https://github.com/tui-rs-revival/ratatui) is a library used to build rich
|
||||
//! terminal users interfaces and dashboards.
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! [ratatui](https://github.com/ratatui-org/ratatui) is a library that is all about cooking up terminal user
|
||||
//! interfaces (TUIs).
|
||||
//!
|
||||
//! 
|
||||
//! 
|
||||
//!
|
||||
//! # Get started
|
||||
//!
|
||||
@@ -10,8 +12,8 @@
|
||||
//! Add the following to your `Cargo.toml`:
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! crossterm = "0.26"
|
||||
//! ratatui = "0.20"
|
||||
//! crossterm = "0.27"
|
||||
//! ratatui = "0.23"
|
||||
//! ```
|
||||
//!
|
||||
//! The crate is using the `crossterm` backend by default that works on most platforms. But if for
|
||||
@@ -20,20 +22,12 @@
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! termion = "1.5"
|
||||
//! ratatui = { version = "0.20", default-features = false, features = ['termion'] }
|
||||
//! termion = "2.0.1"
|
||||
//! ratatui = { version = "0.23", default-features = false, features = ['termion'] }
|
||||
//! ```
|
||||
//!
|
||||
//! The same logic applies for all other available backends.
|
||||
//!
|
||||
//! ### Features
|
||||
//!
|
||||
//! Widgets which add dependencies are gated behind feature flags to prevent unused transitive
|
||||
//! dependencies. The available features are:
|
||||
//!
|
||||
//! * `widget-calendar` - enables [`widgets::calendar`] and adds a dependency on the [time
|
||||
//! crate](https://crates.io/crates/time).
|
||||
//!
|
||||
//! ## Creating a `Terminal`
|
||||
//!
|
||||
//! Every application using `ratatui` should start by instantiating a `Terminal`. It is a light
|
||||
@@ -78,8 +72,8 @@
|
||||
//! implement your own.
|
||||
//!
|
||||
//! Each widget follows a builder pattern API providing a default configuration along with methods
|
||||
//! to customize them. The widget is then rendered using [`Frame::render_widget`] which takes
|
||||
//! your widget instance and an area to draw to.
|
||||
//! to customize them. The widget is then rendered using [`Frame::render_widget`] which takes your
|
||||
//! widget instance and an area to draw to.
|
||||
//!
|
||||
//! The following example renders a block of the size of the terminal:
|
||||
//!
|
||||
@@ -169,11 +163,25 @@
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! This let you describe responsive terminal UI by nesting layouts. You should note that by
|
||||
//! default the computed layout tries to fill the available space completely. So if for any reason
|
||||
//! you might need a blank space somewhere, try to pass an additional constraint and don't use the
|
||||
//! This let you describe responsive terminal UI by nesting layouts. You should note that by default
|
||||
//! the computed layout tries to fill the available space completely. So if for any reason you might
|
||||
//! need a blank space somewhere, try to pass an additional constraint and don't use the
|
||||
//! corresponding area.
|
||||
|
||||
//!
|
||||
//! # Features
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
//!
|
||||
//! [`Layout`]: layout::Layout
|
||||
//! [`backend`]: backend
|
||||
//! [`calendar`]: widgets::calendar
|
||||
//! [`CrosstermBackend`]: backend::CrosstermBackend
|
||||
//! [`TermionBackend`]: backend::TermionBackend
|
||||
//! [`TermwizBackend`]: backend::TermwizBackend
|
||||
//! [Crossterm crate]: https://crates.io/crates/crossterm
|
||||
//! [Serde crate]: https://crates.io/crates/serde
|
||||
//! [Termion crate]: https://crates.io/crates/termion
|
||||
//! [Termwiz crate]: https://crates.io/crates/termwiz
|
||||
//! [Time crate]: https://crates.io/crates/time
|
||||
// show the feature flags in the generated documentation
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
||||
@@ -187,3 +195,5 @@ pub mod text;
|
||||
pub mod widgets;
|
||||
|
||||
pub use self::terminal::{Frame, Terminal, TerminalOptions, Viewport};
|
||||
|
||||
pub mod prelude;
|
||||
|
||||
35
src/prelude.rs
Normal file
35
src/prelude.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
//! A prelude for conveniently writing applications using this library.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use ratatui::prelude::*;
|
||||
//! ```
|
||||
//!
|
||||
//! Aside from the main types that are used in the library, this prelude also re-exports several
|
||||
//! modules to make it easy to qualify types that would otherwise collide. E.g.:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use ratatui::{prelude::*, widgets::*};
|
||||
//! use ratatui::widgets::{Block, Borders};
|
||||
//!
|
||||
//! #[derive(Debug, Default, PartialEq, Eq)]
|
||||
//! struct Line;
|
||||
//!
|
||||
//! assert_eq!(Line::default(), Line);
|
||||
//! assert_eq!(text::Line::default(), ratatui::text::Line::from(vec![]));
|
||||
//! ```
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub use crate::backend::CrosstermBackend;
|
||||
#[cfg(feature = "termion")]
|
||||
pub use crate::backend::TermionBackend;
|
||||
#[cfg(feature = "termwiz")]
|
||||
pub use crate::backend::TermwizBackend;
|
||||
pub use crate::{
|
||||
backend::{self, Backend},
|
||||
buffer::{self, Buffer},
|
||||
layout::{self, Alignment, Constraint, Corner, Direction, Layout, Margin, Rect},
|
||||
style::{self, Color, Modifier, Style, Styled, Stylize},
|
||||
symbols::{self, Marker},
|
||||
terminal::{self, Frame, Terminal, TerminalOptions, Viewport},
|
||||
text::{self, Line, Masked, Span, Text},
|
||||
};
|
||||
544
src/style.rs
544
src/style.rs
@@ -15,50 +15,126 @@
|
||||
//!
|
||||
//! # Using style shorthands
|
||||
//!
|
||||
//! This is best for consise styling.
|
||||
//! This is best for concise styling.
|
||||
//! ## Example
|
||||
//! ```
|
||||
//! use ratatui::{
|
||||
//! style::{Color, Modifier, Style, Styled, Stylize},
|
||||
//! text::Span,
|
||||
//! };
|
||||
//! use ratatui::prelude::*;
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! "hello".red().on_blue().bold(),
|
||||
//! Span::styled("hello", Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD))
|
||||
//! )
|
||||
//! ```
|
||||
mod stylized;
|
||||
|
||||
use std::{
|
||||
fmt::{self, Debug},
|
||||
fmt::{self, Debug, Display},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use bitflags::bitflags;
|
||||
pub use stylized::{Styled, Stylize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
mod stylize;
|
||||
pub use stylize::{Styled, Stylize};
|
||||
|
||||
/// ANSI Color
|
||||
///
|
||||
/// All colors from the [ANSI color table](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors)
|
||||
/// are supported (though some names are not exactly the same).
|
||||
///
|
||||
/// | Color Name | Color | Foreground | Background |
|
||||
/// |----------------|-------------------------|------------|------------|
|
||||
/// | `black` | [`Color::Black`] | 30 | 40 |
|
||||
/// | `red` | [`Color::Red`] | 31 | 41 |
|
||||
/// | `green` | [`Color::Green`] | 32 | 42 |
|
||||
/// | `yellow` | [`Color::Yellow`] | 33 | 43 |
|
||||
/// | `blue` | [`Color::Blue`] | 34 | 44 |
|
||||
/// | `magenta` | [`Color::Magenta`] | 35 | 45 |
|
||||
/// | `cyan` | [`Color::Cyan`] | 36 | 46 |
|
||||
/// | `gray`* | [`Color::Gray`] | 37 | 47 |
|
||||
/// | `darkgray`* | [`Color::DarkGray`] | 90 | 100 |
|
||||
/// | `lightred` | [`Color::LightRed`] | 91 | 101 |
|
||||
/// | `lightgreen` | [`Color::LightGreen`] | 92 | 102 |
|
||||
/// | `lightyellow` | [`Color::LightYellow`] | 93 | 103 |
|
||||
/// | `lightblue` | [`Color::LightBlue`] | 94 | 104 |
|
||||
/// | `lightmagenta` | [`Color::LightMagenta`] | 95 | 105 |
|
||||
/// | `lightcyan` | [`Color::LightCyan`] | 96 | 106 |
|
||||
/// | `white`* | [`Color::White`] | 97 | 107 |
|
||||
///
|
||||
/// - `gray` is sometimes called `white` - this is not supported as we use `white` for bright white
|
||||
/// - `gray` is sometimes called `silver` - this is supported
|
||||
/// - `darkgray` is sometimes called `light black` or `bright black` (both are supported)
|
||||
/// - `white` is sometimes called `light white` or `bright white` (both are supported)
|
||||
/// - we support `bright` and `light` prefixes for all colors
|
||||
/// - we support `-` and `_` and ` ` as separators for all colors
|
||||
/// - we support both `gray` and `grey` spellings
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::style::Color;
|
||||
/// use std::str::FromStr;
|
||||
/// assert_eq!(Color::from_str("red"), Ok(Color::Red));
|
||||
/// assert_eq!("red".parse(), Ok(Color::Red));
|
||||
/// assert_eq!("lightred".parse(), Ok(Color::LightRed));
|
||||
/// assert_eq!("light red".parse(), Ok(Color::LightRed));
|
||||
/// assert_eq!("light-red".parse(), Ok(Color::LightRed));
|
||||
/// assert_eq!("light_red".parse(), Ok(Color::LightRed));
|
||||
/// assert_eq!("lightRed".parse(), Ok(Color::LightRed));
|
||||
/// assert_eq!("bright red".parse(), Ok(Color::LightRed));
|
||||
/// assert_eq!("bright-red".parse(), Ok(Color::LightRed));
|
||||
/// assert_eq!("silver".parse(), Ok(Color::Gray));
|
||||
/// assert_eq!("dark-grey".parse(), Ok(Color::DarkGray));
|
||||
/// assert_eq!("dark gray".parse(), Ok(Color::DarkGray));
|
||||
/// assert_eq!("light-black".parse(), Ok(Color::DarkGray));
|
||||
/// assert_eq!("white".parse(), Ok(Color::White));
|
||||
/// assert_eq!("bright white".parse(), Ok(Color::White));
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Color {
|
||||
/// Resets the foreground or background color
|
||||
#[default]
|
||||
Reset,
|
||||
/// ANSI Color: Black. Foreground: 30, Background: 40
|
||||
Black,
|
||||
/// ANSI Color: Red. Foreground: 31, Background: 41
|
||||
Red,
|
||||
/// ANSI Color: Green. Foreground: 32, Background: 42
|
||||
Green,
|
||||
/// ANSI Color: Yellow. Foreground: 33, Background: 43
|
||||
Yellow,
|
||||
/// ANSI Color: Blue. Foreground: 34, Background: 44
|
||||
Blue,
|
||||
/// ANSI Color: Magenta. Foreground: 35, Background: 45
|
||||
Magenta,
|
||||
/// ANSI Color: Cyan. Foreground: 36, Background: 46
|
||||
Cyan,
|
||||
/// ANSI Color: White. Foreground: 37, Background: 47
|
||||
///
|
||||
/// Note that this is sometimes called `silver` or `white` but we use `white` for bright white
|
||||
Gray,
|
||||
/// ANSI Color: Bright Black. Foreground: 90, Background: 100
|
||||
///
|
||||
/// Note that this is sometimes called `light black` or `bright black` but we use `dark gray`
|
||||
DarkGray,
|
||||
/// ANSI Color: Bright Red. Foreground: 91, Background: 101
|
||||
LightRed,
|
||||
/// ANSI Color: Bright Green. Foreground: 92, Background: 102
|
||||
LightGreen,
|
||||
/// ANSI Color: Bright Yellow. Foreground: 93, Background: 103
|
||||
LightYellow,
|
||||
/// ANSI Color: Bright Blue. Foreground: 94, Background: 104
|
||||
LightBlue,
|
||||
/// ANSI Color: Bright Magenta. Foreground: 95, Background: 105
|
||||
LightMagenta,
|
||||
/// ANSI Color: Bright Cyan. Foreground: 96, Background: 106
|
||||
LightCyan,
|
||||
/// ANSI Color: Bright White. Foreground: 97, Background: 107
|
||||
/// Sometimes called `bright white` or `light white` in some terminals
|
||||
White,
|
||||
/// An RGB color
|
||||
Rgb(u8, u8, u8),
|
||||
/// An 8-bit 256 color. See <https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit>
|
||||
Indexed(u8),
|
||||
}
|
||||
|
||||
@@ -75,7 +151,7 @@ bitflags! {
|
||||
/// let m = Modifier::BOLD | Modifier::ITALIC;
|
||||
/// ```
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct Modifier: u16 {
|
||||
const BOLD = 0b0000_0000_0001;
|
||||
const DIM = 0b0000_0000_0010;
|
||||
@@ -123,7 +199,9 @@ impl fmt::Debug for Modifier {
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let styles = [
|
||||
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::default().bg(Color::Red),
|
||||
/// Style::default().bg(Color::Red).add_modifier(Modifier::UNDERLINED),
|
||||
/// #[cfg(feature = "crossterm")]
|
||||
/// Style::default().underline_color(Color::Green),
|
||||
/// Style::default().fg(Color::Yellow).remove_modifier(Modifier::ITALIC),
|
||||
/// ];
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
@@ -134,7 +212,9 @@ impl fmt::Debug for Modifier {
|
||||
/// Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Red),
|
||||
/// add_modifier: Modifier::BOLD,
|
||||
/// #[cfg(feature = "crossterm")]
|
||||
/// underline_color: Some(Color::Green),
|
||||
/// add_modifier: Modifier::BOLD | Modifier::UNDERLINED,
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// buffer.get(0, 0).style(),
|
||||
@@ -160,17 +240,21 @@ impl fmt::Debug for Modifier {
|
||||
/// Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Reset),
|
||||
/// #[cfg(feature = "crossterm")]
|
||||
/// underline_color: Some(Color::Reset),
|
||||
/// add_modifier: Modifier::empty(),
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// buffer.get(0, 0).style(),
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Style {
|
||||
pub fg: Option<Color>,
|
||||
pub bg: Option<Color>,
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub underline_color: Option<Color>,
|
||||
pub add_modifier: Modifier,
|
||||
pub sub_modifier: Modifier,
|
||||
}
|
||||
@@ -181,11 +265,24 @@ impl Default for Style {
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Style {
|
||||
type Item = Style;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
*self
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
self.patch(style)
|
||||
}
|
||||
}
|
||||
impl Style {
|
||||
pub const fn new() -> Style {
|
||||
Style {
|
||||
fg: None,
|
||||
bg: None,
|
||||
#[cfg(feature = "crossterm")]
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::empty(),
|
||||
}
|
||||
@@ -196,6 +293,8 @@ impl Style {
|
||||
Style {
|
||||
fg: Some(Color::Reset),
|
||||
bg: Some(Color::Reset),
|
||||
#[cfg(feature = "crossterm")]
|
||||
underline_color: Some(Color::Reset),
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::all(),
|
||||
}
|
||||
@@ -231,6 +330,27 @@ impl Style {
|
||||
self
|
||||
}
|
||||
|
||||
/// Changes the underline color. The text must be underlined with a modifier for this to work.
|
||||
///
|
||||
/// This uses a non-standard ANSI escape sequence. It is supported by most terminal emulators,
|
||||
/// but is only implemented in the crossterm backend.
|
||||
///
|
||||
/// See [Wikipedia](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters) code `58` and `59` for more information.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().underline_color(Color::Blue).add_modifier(Modifier::UNDERLINED);
|
||||
/// let diff = Style::default().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED);
|
||||
/// assert_eq!(style.patch(diff), Style::default().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED));
|
||||
/// ```
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub const fn underline_color(mut self, color: Color) -> Style {
|
||||
self.underline_color = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
/// Changes the text emphasis.
|
||||
///
|
||||
/// When applied, it adds the given modifier to the `Style` modifiers.
|
||||
@@ -245,9 +365,9 @@ impl Style {
|
||||
/// assert_eq!(patched.add_modifier, Modifier::BOLD | Modifier::ITALIC);
|
||||
/// assert_eq!(patched.sub_modifier, Modifier::empty());
|
||||
/// ```
|
||||
pub fn add_modifier(mut self, modifier: Modifier) -> Style {
|
||||
self.sub_modifier.remove(modifier);
|
||||
self.add_modifier.insert(modifier);
|
||||
pub const fn add_modifier(mut self, modifier: Modifier) -> Style {
|
||||
self.sub_modifier = self.sub_modifier.difference(modifier);
|
||||
self.add_modifier = self.add_modifier.union(modifier);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -265,9 +385,9 @@ impl Style {
|
||||
/// assert_eq!(patched.add_modifier, Modifier::BOLD);
|
||||
/// assert_eq!(patched.sub_modifier, Modifier::ITALIC);
|
||||
/// ```
|
||||
pub fn remove_modifier(mut self, modifier: Modifier) -> Style {
|
||||
self.add_modifier.remove(modifier);
|
||||
self.sub_modifier.insert(modifier);
|
||||
pub const fn remove_modifier(mut self, modifier: Modifier) -> Style {
|
||||
self.add_modifier = self.add_modifier.difference(modifier);
|
||||
self.sub_modifier = self.sub_modifier.union(modifier);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -288,6 +408,11 @@ impl Style {
|
||||
self.fg = other.fg.or(self.fg);
|
||||
self.bg = other.bg.or(self.bg);
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
{
|
||||
self.underline_color = other.underline_color.or(self.underline_color);
|
||||
}
|
||||
|
||||
self.add_modifier.remove(other.sub_modifier);
|
||||
self.add_modifier.insert(other.add_modifier);
|
||||
self.sub_modifier.remove(other.add_modifier);
|
||||
@@ -298,7 +423,7 @@ impl Style {
|
||||
}
|
||||
|
||||
/// Error type indicating a failure to parse a color string.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct ParseColorError;
|
||||
|
||||
impl std::fmt::Display for ParseColorError {
|
||||
@@ -311,9 +436,11 @@ impl std::error::Error for ParseColorError {}
|
||||
|
||||
/// Converts a string representation to a `Color` instance.
|
||||
///
|
||||
/// The `from_str` function attempts to parse the given string and convert it
|
||||
/// to the corresponding `Color` variant. It supports named colors, RGB values,
|
||||
/// and indexed colors. If the string cannot be parsed, a `ParseColorError` is returned.
|
||||
/// The `from_str` function attempts to parse the given string and convert it to the corresponding
|
||||
/// `Color` variant. It supports named colors, RGB values, and indexed colors. If the string cannot
|
||||
/// be parsed, a `ParseColorError` is returned.
|
||||
///
|
||||
/// See the [`Color`] documentation for more information on the supported color names.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -336,48 +463,90 @@ impl FromStr for Color {
|
||||
type Err = ParseColorError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match s.to_lowercase().as_ref() {
|
||||
"reset" => Self::Reset,
|
||||
"black" => Self::Black,
|
||||
"red" => Self::Red,
|
||||
"green" => Self::Green,
|
||||
"yellow" => Self::Yellow,
|
||||
"blue" => Self::Blue,
|
||||
"magenta" => Self::Magenta,
|
||||
"cyan" => Self::Cyan,
|
||||
"gray" => Self::Gray,
|
||||
"darkgray" | "dark gray" => Self::DarkGray,
|
||||
"lightred" | "light red" => Self::LightRed,
|
||||
"lightgreen" | "light green" => Self::LightGreen,
|
||||
"lightyellow" | "light yellow" => Self::LightYellow,
|
||||
"lightblue" | "light blue" => Self::LightBlue,
|
||||
"lightmagenta" | "light magenta" => Self::LightMagenta,
|
||||
"lightcyan" | "light cyan" => Self::LightCyan,
|
||||
"white" => Self::White,
|
||||
_ => {
|
||||
if let Ok(index) = s.parse::<u8>() {
|
||||
Self::Indexed(index)
|
||||
} else if let (Ok(r), Ok(g), Ok(b)) = {
|
||||
if !s.starts_with('#') || s.len() != 7 {
|
||||
Ok(
|
||||
// There is a mix of different color names and formats in the wild.
|
||||
// This is an attempt to support as many as possible.
|
||||
match s
|
||||
.to_lowercase()
|
||||
.replace([' ', '-', '_'], "")
|
||||
.replace("bright", "light")
|
||||
.replace("grey", "gray")
|
||||
.replace("silver", "gray")
|
||||
.replace("lightblack", "darkgray")
|
||||
.replace("lightwhite", "white")
|
||||
.replace("lightgray", "white")
|
||||
.as_ref()
|
||||
{
|
||||
"reset" => Self::Reset,
|
||||
"black" => Self::Black,
|
||||
"red" => Self::Red,
|
||||
"green" => Self::Green,
|
||||
"yellow" => Self::Yellow,
|
||||
"blue" => Self::Blue,
|
||||
"magenta" => Self::Magenta,
|
||||
"cyan" => Self::Cyan,
|
||||
"gray" => Self::Gray,
|
||||
"darkgray" => Self::DarkGray,
|
||||
"lightred" => Self::LightRed,
|
||||
"lightgreen" => Self::LightGreen,
|
||||
"lightyellow" => Self::LightYellow,
|
||||
"lightblue" => Self::LightBlue,
|
||||
"lightmagenta" => Self::LightMagenta,
|
||||
"lightcyan" => Self::LightCyan,
|
||||
"white" => Self::White,
|
||||
_ => {
|
||||
if let Ok(index) = s.parse::<u8>() {
|
||||
Self::Indexed(index)
|
||||
} else if let (Ok(r), Ok(g), Ok(b)) = {
|
||||
if !s.starts_with('#') || s.len() != 7 {
|
||||
return Err(ParseColorError);
|
||||
}
|
||||
(
|
||||
u8::from_str_radix(&s[1..3], 16),
|
||||
u8::from_str_radix(&s[3..5], 16),
|
||||
u8::from_str_radix(&s[5..7], 16),
|
||||
)
|
||||
} {
|
||||
Self::Rgb(r, g, b)
|
||||
} else {
|
||||
return Err(ParseColorError);
|
||||
}
|
||||
(
|
||||
u8::from_str_radix(&s[1..3], 16),
|
||||
u8::from_str_radix(&s[3..5], 16),
|
||||
u8::from_str_radix(&s[5..7], 16),
|
||||
)
|
||||
} {
|
||||
Self::Rgb(r, g, b)
|
||||
} else {
|
||||
return Err(ParseColorError);
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Color {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Color::Reset => write!(f, "Reset"),
|
||||
Color::Black => write!(f, "Black"),
|
||||
Color::Red => write!(f, "Red"),
|
||||
Color::Green => write!(f, "Green"),
|
||||
Color::Yellow => write!(f, "Yellow"),
|
||||
Color::Blue => write!(f, "Blue"),
|
||||
Color::Magenta => write!(f, "Magenta"),
|
||||
Color::Cyan => write!(f, "Cyan"),
|
||||
Color::Gray => write!(f, "Gray"),
|
||||
Color::DarkGray => write!(f, "DarkGray"),
|
||||
Color::LightRed => write!(f, "LightRed"),
|
||||
Color::LightGreen => write!(f, "LightGreen"),
|
||||
Color::LightYellow => write!(f, "LightYellow"),
|
||||
Color::LightBlue => write!(f, "LightBlue"),
|
||||
Color::LightMagenta => write!(f, "LightMagenta"),
|
||||
Color::LightCyan => write!(f, "LightCyan"),
|
||||
Color::White => write!(f, "White"),
|
||||
Color::Rgb(r, g, b) => write!(f, "#{:02X}{:02X}{:02X}", r, g, b),
|
||||
Color::Indexed(i) => write!(f, "{}", i),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::error::Error;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn styles() -> Vec<Style> {
|
||||
@@ -443,7 +612,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modifier_debug() {
|
||||
fn modifier_debug() {
|
||||
assert_eq!(format!("{:?}", Modifier::empty()), "NONE");
|
||||
assert_eq!(format!("{:?}", Modifier::BOLD), "BOLD");
|
||||
assert_eq!(format!("{:?}", Modifier::DIM), "DIM");
|
||||
@@ -465,32 +634,81 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rgb_color() {
|
||||
fn from_rgb_color() {
|
||||
let color: Color = Color::from_str("#FF0000").unwrap();
|
||||
assert_eq!(color, Color::Rgb(255, 0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indexed_color() {
|
||||
fn from_indexed_color() {
|
||||
let color: Color = Color::from_str("10").unwrap();
|
||||
assert_eq!(color, Color::Indexed(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_color() {
|
||||
let color: Color = Color::from_str("lightblue").unwrap();
|
||||
assert_eq!(color, Color::LightBlue);
|
||||
fn from_ansi_color() -> Result<(), Box<dyn Error>> {
|
||||
assert_eq!(Color::from_str("reset")?, Color::Reset);
|
||||
assert_eq!(Color::from_str("black")?, Color::Black);
|
||||
assert_eq!(Color::from_str("red")?, Color::Red);
|
||||
assert_eq!(Color::from_str("green")?, Color::Green);
|
||||
assert_eq!(Color::from_str("yellow")?, Color::Yellow);
|
||||
assert_eq!(Color::from_str("blue")?, Color::Blue);
|
||||
assert_eq!(Color::from_str("magenta")?, Color::Magenta);
|
||||
assert_eq!(Color::from_str("cyan")?, Color::Cyan);
|
||||
assert_eq!(Color::from_str("gray")?, Color::Gray);
|
||||
assert_eq!(Color::from_str("darkgray")?, Color::DarkGray);
|
||||
assert_eq!(Color::from_str("lightred")?, Color::LightRed);
|
||||
assert_eq!(Color::from_str("lightgreen")?, Color::LightGreen);
|
||||
assert_eq!(Color::from_str("lightyellow")?, Color::LightYellow);
|
||||
assert_eq!(Color::from_str("lightblue")?, Color::LightBlue);
|
||||
assert_eq!(Color::from_str("lightmagenta")?, Color::LightMagenta);
|
||||
assert_eq!(Color::from_str("lightcyan")?, Color::LightCyan);
|
||||
assert_eq!(Color::from_str("white")?, Color::White);
|
||||
|
||||
// aliases
|
||||
assert_eq!(Color::from_str("lightblack")?, Color::DarkGray);
|
||||
assert_eq!(Color::from_str("lightwhite")?, Color::White);
|
||||
assert_eq!(Color::from_str("lightgray")?, Color::White);
|
||||
|
||||
// silver = grey = gray
|
||||
assert_eq!(Color::from_str("grey")?, Color::Gray);
|
||||
assert_eq!(Color::from_str("silver")?, Color::Gray);
|
||||
|
||||
// spaces are ignored
|
||||
assert_eq!(Color::from_str("light black")?, Color::DarkGray);
|
||||
assert_eq!(Color::from_str("light white")?, Color::White);
|
||||
assert_eq!(Color::from_str("light gray")?, Color::White);
|
||||
|
||||
// dashes are ignored
|
||||
assert_eq!(Color::from_str("light-black")?, Color::DarkGray);
|
||||
assert_eq!(Color::from_str("light-white")?, Color::White);
|
||||
assert_eq!(Color::from_str("light-gray")?, Color::White);
|
||||
|
||||
// underscores are ignored
|
||||
assert_eq!(Color::from_str("light_black")?, Color::DarkGray);
|
||||
assert_eq!(Color::from_str("light_white")?, Color::White);
|
||||
assert_eq!(Color::from_str("light_gray")?, Color::White);
|
||||
|
||||
// bright = light
|
||||
assert_eq!(Color::from_str("bright-black")?, Color::DarkGray);
|
||||
assert_eq!(Color::from_str("bright-white")?, Color::White);
|
||||
|
||||
// bright = light
|
||||
assert_eq!(Color::from_str("brightblack")?, Color::DarkGray);
|
||||
assert_eq!(Color::from_str("brightwhite")?, Color::White);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_colors() {
|
||||
fn from_invalid_colors() {
|
||||
let bad_colors = [
|
||||
"invalid_color", // not a color string
|
||||
"abcdef0", // 7 chars is not a color
|
||||
" bcdefa", // doesn't start with a '#'
|
||||
"blue ", // has space at end
|
||||
" blue", // has space at start
|
||||
"#abcdef00", // too many chars
|
||||
"resett", // typo
|
||||
"lightblackk", // typo
|
||||
];
|
||||
|
||||
for bad_color in bad_colors {
|
||||
@@ -500,9 +718,201 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display() {
|
||||
assert_eq!(format!("{}", Color::Black), "Black");
|
||||
assert_eq!(format!("{}", Color::Red), "Red");
|
||||
assert_eq!(format!("{}", Color::Green), "Green");
|
||||
assert_eq!(format!("{}", Color::Yellow), "Yellow");
|
||||
assert_eq!(format!("{}", Color::Blue), "Blue");
|
||||
assert_eq!(format!("{}", Color::Magenta), "Magenta");
|
||||
assert_eq!(format!("{}", Color::Cyan), "Cyan");
|
||||
assert_eq!(format!("{}", Color::Gray), "Gray");
|
||||
assert_eq!(format!("{}", Color::DarkGray), "DarkGray");
|
||||
assert_eq!(format!("{}", Color::LightRed), "LightRed");
|
||||
assert_eq!(format!("{}", Color::LightGreen), "LightGreen");
|
||||
assert_eq!(format!("{}", Color::LightYellow), "LightYellow");
|
||||
assert_eq!(format!("{}", Color::LightBlue), "LightBlue");
|
||||
assert_eq!(format!("{}", Color::LightMagenta), "LightMagenta");
|
||||
assert_eq!(format!("{}", Color::LightCyan), "LightCyan");
|
||||
assert_eq!(format!("{}", Color::White), "White");
|
||||
assert_eq!(format!("{}", Color::Indexed(10)), "10");
|
||||
assert_eq!(format!("{}", Color::Rgb(255, 0, 0)), "#FF0000");
|
||||
assert_eq!(format!("{}", Color::Reset), "Reset");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_can_be_const() {
|
||||
const _DEFAULT_STYLE: Style = Style::new().fg(Color::Red).bg(Color::Black);
|
||||
const RED: Color = Color::Red;
|
||||
const BLACK: Color = Color::Black;
|
||||
const BOLD: Modifier = Modifier::BOLD;
|
||||
const ITALIC: Modifier = Modifier::ITALIC;
|
||||
|
||||
const _RESET: Style = Style::reset();
|
||||
const _RED_FG: Style = Style::new().fg(RED);
|
||||
const _BLACK_BG: Style = Style::new().bg(BLACK);
|
||||
const _ADD_BOLD: Style = Style::new().add_modifier(BOLD);
|
||||
const _REMOVE_ITALIC: Style = Style::new().remove_modifier(ITALIC);
|
||||
const ALL: Style = Style::new()
|
||||
.fg(RED)
|
||||
.bg(BLACK)
|
||||
.add_modifier(BOLD)
|
||||
.remove_modifier(ITALIC);
|
||||
assert_eq!(
|
||||
ALL,
|
||||
Style::new()
|
||||
.fg(Color::Red)
|
||||
.bg(Color::Black)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.remove_modifier(Modifier::ITALIC)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_can_be_stylized() {
|
||||
// foreground colors
|
||||
assert_eq!(Style::new().black(), Style::new().fg(Color::Black));
|
||||
assert_eq!(Style::new().red(), Style::new().fg(Color::Red));
|
||||
assert_eq!(Style::new().green(), Style::new().fg(Color::Green));
|
||||
assert_eq!(Style::new().yellow(), Style::new().fg(Color::Yellow));
|
||||
assert_eq!(Style::new().blue(), Style::new().fg(Color::Blue));
|
||||
assert_eq!(Style::new().magenta(), Style::new().fg(Color::Magenta));
|
||||
assert_eq!(Style::new().cyan(), Style::new().fg(Color::Cyan));
|
||||
assert_eq!(Style::new().white(), Style::new().fg(Color::White));
|
||||
assert_eq!(Style::new().gray(), Style::new().fg(Color::Gray));
|
||||
assert_eq!(Style::new().dark_gray(), Style::new().fg(Color::DarkGray));
|
||||
assert_eq!(Style::new().light_red(), Style::new().fg(Color::LightRed));
|
||||
assert_eq!(
|
||||
Style::new().light_green(),
|
||||
Style::new().fg(Color::LightGreen)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().light_yellow(),
|
||||
Style::new().fg(Color::LightYellow)
|
||||
);
|
||||
assert_eq!(Style::new().light_blue(), Style::new().fg(Color::LightBlue));
|
||||
assert_eq!(
|
||||
Style::new().light_magenta(),
|
||||
Style::new().fg(Color::LightMagenta)
|
||||
);
|
||||
assert_eq!(Style::new().light_cyan(), Style::new().fg(Color::LightCyan));
|
||||
assert_eq!(Style::new().white(), Style::new().fg(Color::White));
|
||||
|
||||
// Background colors
|
||||
assert_eq!(Style::new().on_black(), Style::new().bg(Color::Black));
|
||||
assert_eq!(Style::new().on_red(), Style::new().bg(Color::Red));
|
||||
assert_eq!(Style::new().on_green(), Style::new().bg(Color::Green));
|
||||
assert_eq!(Style::new().on_yellow(), Style::new().bg(Color::Yellow));
|
||||
assert_eq!(Style::new().on_blue(), Style::new().bg(Color::Blue));
|
||||
assert_eq!(Style::new().on_magenta(), Style::new().bg(Color::Magenta));
|
||||
assert_eq!(Style::new().on_cyan(), Style::new().bg(Color::Cyan));
|
||||
assert_eq!(Style::new().on_white(), Style::new().bg(Color::White));
|
||||
assert_eq!(Style::new().on_gray(), Style::new().bg(Color::Gray));
|
||||
assert_eq!(
|
||||
Style::new().on_dark_gray(),
|
||||
Style::new().bg(Color::DarkGray)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_red(),
|
||||
Style::new().bg(Color::LightRed)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_green(),
|
||||
Style::new().bg(Color::LightGreen)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_yellow(),
|
||||
Style::new().bg(Color::LightYellow)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_blue(),
|
||||
Style::new().bg(Color::LightBlue)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_magenta(),
|
||||
Style::new().bg(Color::LightMagenta)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_cyan(),
|
||||
Style::new().bg(Color::LightCyan)
|
||||
);
|
||||
assert_eq!(Style::new().on_white(), Style::new().bg(Color::White));
|
||||
|
||||
// Add Modifiers
|
||||
assert_eq!(
|
||||
Style::new().bold(),
|
||||
Style::new().add_modifier(Modifier::BOLD)
|
||||
);
|
||||
assert_eq!(Style::new().dim(), Style::new().add_modifier(Modifier::DIM));
|
||||
assert_eq!(
|
||||
Style::new().italic(),
|
||||
Style::new().add_modifier(Modifier::ITALIC)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().underlined(),
|
||||
Style::new().add_modifier(Modifier::UNDERLINED)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().slow_blink(),
|
||||
Style::new().add_modifier(Modifier::SLOW_BLINK)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().rapid_blink(),
|
||||
Style::new().add_modifier(Modifier::RAPID_BLINK)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().reversed(),
|
||||
Style::new().add_modifier(Modifier::REVERSED)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().hidden(),
|
||||
Style::new().add_modifier(Modifier::HIDDEN)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().crossed_out(),
|
||||
Style::new().add_modifier(Modifier::CROSSED_OUT)
|
||||
);
|
||||
|
||||
// Remove Modifiers
|
||||
assert_eq!(
|
||||
Style::new().not_bold(),
|
||||
Style::new().remove_modifier(Modifier::BOLD)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_dim(),
|
||||
Style::new().remove_modifier(Modifier::DIM)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_italic(),
|
||||
Style::new().remove_modifier(Modifier::ITALIC)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_underlined(),
|
||||
Style::new().remove_modifier(Modifier::UNDERLINED)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_slow_blink(),
|
||||
Style::new().remove_modifier(Modifier::SLOW_BLINK)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_rapid_blink(),
|
||||
Style::new().remove_modifier(Modifier::RAPID_BLINK)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_reversed(),
|
||||
Style::new().remove_modifier(Modifier::REVERSED)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_hidden(),
|
||||
Style::new().remove_modifier(Modifier::HIDDEN)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_crossed_out(),
|
||||
Style::new().remove_modifier(Modifier::CROSSED_OUT)
|
||||
);
|
||||
|
||||
// reset
|
||||
assert_eq!(Style::new().reset(), Style::reset());
|
||||
}
|
||||
}
|
||||
|
||||
260
src/style/stylize.rs
Normal file
260
src/style/stylize.rs
Normal file
@@ -0,0 +1,260 @@
|
||||
use paste::paste;
|
||||
|
||||
use crate::{
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
};
|
||||
|
||||
/// A trait for objects that have a `Style`.
|
||||
///
|
||||
/// This trait enables generic code to be written that can interact with any object that has a
|
||||
/// `Style`. This is used by the `Stylize` trait to allow generic code to be written that can
|
||||
/// interact with any object that can be styled.
|
||||
pub trait Styled {
|
||||
type Item;
|
||||
|
||||
fn style(&self) -> Style;
|
||||
fn set_style(self, style: Style) -> Self::Item;
|
||||
}
|
||||
|
||||
/// Generates two methods for each color, one for setting the foreground color (`red()`, `blue()`,
|
||||
/// etc) and one for setting the background color (`on_red()`, `on_blue()`, etc.). Each method sets
|
||||
/// the color of the style to the corresponding color.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// color!(black);
|
||||
///
|
||||
/// // generates
|
||||
///
|
||||
/// #[doc = "Sets the foreground color to [`black`](Color::Black)."]
|
||||
/// fn black(self) -> T {
|
||||
/// self.fg(Color::Black)
|
||||
/// }
|
||||
///
|
||||
/// #[doc = "Sets the background color to [`black`](Color::Black)."]
|
||||
/// fn on_black(self) -> T {
|
||||
/// self.bg(Color::Black)
|
||||
/// }
|
||||
/// ```
|
||||
macro_rules! color {
|
||||
( $color:ident ) => {
|
||||
paste! {
|
||||
#[doc = "Sets the foreground color to [`" $color "`](Color::" $color:camel ")."]
|
||||
fn $color(self) -> T {
|
||||
self.fg(Color::[<$color:camel>])
|
||||
}
|
||||
|
||||
#[doc = "Sets the background color to [`" $color "`](Color::" $color:camel ")."]
|
||||
fn [<on_ $color>](self) -> T {
|
||||
self.bg(Color::[<$color:camel>])
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Generates a method for a modifier (`bold()`, `italic()`, etc.). Each method sets the modifier
|
||||
/// of the style to the corresponding modifier.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// modifier!(bold);
|
||||
///
|
||||
/// // generates
|
||||
///
|
||||
/// #[doc = "Adds the [`BOLD`](Modifier::BOLD) modifier."]
|
||||
/// fn bold(self) -> T {
|
||||
/// self.add_modifier(Modifier::BOLD)
|
||||
/// }
|
||||
///
|
||||
/// #[doc = "Removes the [`BOLD`](Modifier::BOLD) modifier."]
|
||||
/// fn not_bold(self) -> T {
|
||||
/// self.remove_modifier(Modifier::BOLD)
|
||||
/// }
|
||||
/// ```
|
||||
macro_rules! modifier {
|
||||
( $modifier:ident ) => {
|
||||
paste! {
|
||||
#[doc = "Adds the [`" $modifier:upper "`](Modifier::" $modifier:upper ") modifier."]
|
||||
fn [<$modifier>](self) -> T {
|
||||
self.add_modifier(Modifier::[<$modifier:upper>])
|
||||
}
|
||||
}
|
||||
|
||||
paste! {
|
||||
#[doc = "Removes the [`" $modifier:upper "`](Modifier::" $modifier:upper ") modifier."]
|
||||
fn [<not_ $modifier>](self) -> T {
|
||||
self.remove_modifier(Modifier::[<$modifier:upper>])
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// The trait that enables something to be have a style.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// style::{Color, Modifier, Style, Styled, Stylize},
|
||||
/// text::Span,
|
||||
/// };
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// "hello".red().on_blue().bold(),
|
||||
/// Span::styled("hello", Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD))
|
||||
/// )
|
||||
pub trait Stylize<'a, T>: Sized {
|
||||
fn bg(self, color: Color) -> T;
|
||||
fn fg<S: Into<Color>>(self, color: S) -> T;
|
||||
fn reset(self) -> T;
|
||||
fn add_modifier(self, modifier: Modifier) -> T;
|
||||
fn remove_modifier(self, modifier: Modifier) -> T;
|
||||
|
||||
color!(black);
|
||||
color!(red);
|
||||
color!(green);
|
||||
color!(yellow);
|
||||
color!(blue);
|
||||
color!(magenta);
|
||||
color!(cyan);
|
||||
color!(gray);
|
||||
color!(dark_gray);
|
||||
color!(light_red);
|
||||
color!(light_green);
|
||||
color!(light_yellow);
|
||||
color!(light_blue);
|
||||
color!(light_magenta);
|
||||
color!(light_cyan);
|
||||
color!(white);
|
||||
|
||||
modifier!(bold);
|
||||
modifier!(dim);
|
||||
modifier!(italic);
|
||||
modifier!(underlined);
|
||||
modifier!(slow_blink);
|
||||
modifier!(rapid_blink);
|
||||
modifier!(reversed);
|
||||
modifier!(hidden);
|
||||
modifier!(crossed_out);
|
||||
}
|
||||
|
||||
impl<'a, T, U> Stylize<'a, T> for U
|
||||
where
|
||||
U: Styled<Item = T>,
|
||||
{
|
||||
fn bg(self, color: Color) -> T {
|
||||
let style = self.style().bg(color);
|
||||
self.set_style(style)
|
||||
}
|
||||
|
||||
fn fg<S: Into<Color>>(self, color: S) -> T {
|
||||
let style = self.style().fg(color.into());
|
||||
self.set_style(style)
|
||||
}
|
||||
|
||||
fn add_modifier(self, modifier: Modifier) -> T {
|
||||
let style = self.style().add_modifier(modifier);
|
||||
self.set_style(style)
|
||||
}
|
||||
|
||||
fn remove_modifier(self, modifier: Modifier) -> T {
|
||||
let style = self.style().remove_modifier(modifier);
|
||||
self.set_style(style)
|
||||
}
|
||||
|
||||
fn reset(self) -> T {
|
||||
self.set_style(Style::reset())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for &'a str {
|
||||
type Item = Span<'a>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
Style::default()
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
Span::styled(self, style)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn reset() {
|
||||
assert_eq!(
|
||||
"hello".on_cyan().light_red().bold().underlined().reset(),
|
||||
Span::styled("hello", Style::reset())
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fg() {
|
||||
let cyan_fg = Style::default().fg(Color::Cyan);
|
||||
|
||||
assert_eq!("hello".cyan(), Span::styled("hello", cyan_fg));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bg() {
|
||||
let cyan_bg = Style::default().bg(Color::Cyan);
|
||||
|
||||
assert_eq!("hello".on_cyan(), Span::styled("hello", cyan_bg));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_modifier() {
|
||||
let cyan_bold = Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
|
||||
assert_eq!("hello".cyan().bold(), Span::styled("hello", cyan_bold))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fg_bg() {
|
||||
let cyan_fg_bg = Style::default().bg(Color::Cyan).fg(Color::Cyan);
|
||||
|
||||
assert_eq!("hello".cyan().on_cyan(), Span::styled("hello", cyan_fg_bg))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_attributes() {
|
||||
let cyan_bg = Style::default().bg(Color::Cyan);
|
||||
let cyan_fg = Style::default().fg(Color::Cyan);
|
||||
|
||||
// Behavior: the last one set is the definitive one
|
||||
assert_eq!("hello".on_red().on_cyan(), Span::styled("hello", cyan_bg));
|
||||
assert_eq!("hello".red().cyan(), Span::styled("hello", cyan_fg));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_chained() {
|
||||
let all_modifier_black = Style::default()
|
||||
.bg(Color::Black)
|
||||
.fg(Color::Black)
|
||||
.add_modifier(
|
||||
Modifier::UNDERLINED
|
||||
| Modifier::BOLD
|
||||
| Modifier::DIM
|
||||
| Modifier::SLOW_BLINK
|
||||
| Modifier::REVERSED
|
||||
| Modifier::CROSSED_OUT,
|
||||
);
|
||||
assert_eq!(
|
||||
"hello"
|
||||
.on_black()
|
||||
.black()
|
||||
.bold()
|
||||
.underlined()
|
||||
.dim()
|
||||
.slow_blink()
|
||||
.crossed_out()
|
||||
.reversed(),
|
||||
Span::styled("hello", all_modifier_black)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
use crate::{
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
};
|
||||
|
||||
pub trait Styled {
|
||||
type Item;
|
||||
|
||||
fn style(&self) -> Style;
|
||||
fn set_style(self, style: Style) -> Self::Item;
|
||||
}
|
||||
|
||||
// Otherwise rustfmt behaves weirdly for some reason
|
||||
macro_rules! calculated_docs {
|
||||
($(#[doc = $doc:expr] $item:item)*) => { $(#[doc = $doc] $item)* };
|
||||
}
|
||||
|
||||
macro_rules! modifier_method {
|
||||
($method_name:ident Modifier::$modifier:ident) => {
|
||||
calculated_docs! {
|
||||
#[doc = concat!(
|
||||
"Applies the [`",
|
||||
stringify!($modifier),
|
||||
"`](crate::style::Modifier::",
|
||||
stringify!($modifier),
|
||||
") modifier.",
|
||||
)]
|
||||
fn $method_name(self) -> T {
|
||||
self.modifier(Modifier::$modifier)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! color_method {
|
||||
($method_name_fg:ident, $method_name_bg:ident Color::$color:ident) => {
|
||||
calculated_docs! {
|
||||
#[doc = concat!(
|
||||
"Sets the foreground color to [`",
|
||||
stringify!($color),
|
||||
"`](Color::",
|
||||
stringify!($color),
|
||||
")."
|
||||
)]
|
||||
fn $method_name_fg(self) -> T {
|
||||
self.fg(Color::$color)
|
||||
}
|
||||
|
||||
#[doc = concat!(
|
||||
"Sets the background color to [`",
|
||||
stringify!($color),
|
||||
"`](Color::",
|
||||
stringify!($color),
|
||||
")."
|
||||
)]
|
||||
fn $method_name_bg(self) -> T {
|
||||
self.bg(Color::$color)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// The trait that enables something to be have a style.
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// style::{Color, Modifier, Style, Styled, Stylize},
|
||||
/// text::Span,
|
||||
/// };
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// "hello".red().on_blue().bold(),
|
||||
/// Span::styled("hello", Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD))
|
||||
/// )
|
||||
pub trait Stylize<'a, T>: Sized {
|
||||
// Colors
|
||||
fn fg<S: Into<Color>>(self, color: S) -> T;
|
||||
fn bg(self, color: Color) -> T;
|
||||
|
||||
color_method!(black, on_black Color::Black);
|
||||
color_method!(red, on_red Color::Red);
|
||||
color_method!(green, on_green Color::Green);
|
||||
color_method!(yellow, on_yellow Color::Yellow);
|
||||
color_method!(blue, on_blue Color::Blue);
|
||||
color_method!(magenta, on_magenta Color::Magenta);
|
||||
color_method!(cyan, on_cyan Color::Cyan);
|
||||
color_method!(gray, on_gray Color::Gray);
|
||||
color_method!(dark_gray, on_dark_gray Color::DarkGray);
|
||||
color_method!(light_red, on_light_red Color::LightRed);
|
||||
color_method!(light_green, on_light_green Color::LightGreen);
|
||||
color_method!(light_yellow, on_light_yellow Color::LightYellow);
|
||||
color_method!(light_blue, on_light_blue Color::LightBlue);
|
||||
color_method!(light_magenta, on_light_magenta Color::LightMagenta);
|
||||
color_method!(light_cyan, on_light_cyan Color::LightCyan);
|
||||
color_method!(white, on_white Color::White);
|
||||
|
||||
// Styles
|
||||
fn reset(self) -> T;
|
||||
|
||||
// Modifiers
|
||||
fn modifier(self, modifier: Modifier) -> T;
|
||||
|
||||
modifier_method!(bold Modifier::BOLD);
|
||||
modifier_method!(dimmed Modifier::DIM);
|
||||
modifier_method!(italic Modifier::ITALIC);
|
||||
modifier_method!(underline Modifier::UNDERLINED);
|
||||
modifier_method!(slow_blink Modifier::SLOW_BLINK);
|
||||
modifier_method!(rapid_blink Modifier::RAPID_BLINK);
|
||||
modifier_method!(reversed Modifier::REVERSED);
|
||||
modifier_method!(hidden Modifier::HIDDEN);
|
||||
modifier_method!(crossed_out Modifier::CROSSED_OUT);
|
||||
}
|
||||
|
||||
impl<'a, T, U> Stylize<'a, T> for U
|
||||
where
|
||||
U: Styled<Item = T>,
|
||||
{
|
||||
fn fg<S: Into<Color>>(self, color: S) -> T {
|
||||
let style = self.style().fg(color.into());
|
||||
self.set_style(style)
|
||||
}
|
||||
|
||||
fn modifier(self, modifier: Modifier) -> T {
|
||||
let style = self.style().add_modifier(modifier);
|
||||
self.set_style(style)
|
||||
}
|
||||
|
||||
fn bg(self, color: Color) -> T {
|
||||
let style = self.style().bg(color);
|
||||
self.set_style(style)
|
||||
}
|
||||
|
||||
fn reset(self) -> T {
|
||||
self.set_style(Style::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for &'a str {
|
||||
type Item = Span<'a>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
Style::default()
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
Span::styled(self, style)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn reset() {
|
||||
assert_eq!(
|
||||
"hello".on_cyan().light_red().bold().underline().reset(),
|
||||
Span::from("hello")
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fg() {
|
||||
let cyan_fg = Style::default().fg(Color::Cyan);
|
||||
|
||||
assert_eq!("hello".cyan(), Span::styled("hello", cyan_fg));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bg() {
|
||||
let cyan_bg = Style::default().bg(Color::Cyan);
|
||||
|
||||
assert_eq!("hello".on_cyan(), Span::styled("hello", cyan_bg));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_modifier() {
|
||||
let cyan_bold = Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
|
||||
assert_eq!("hello".cyan().bold(), Span::styled("hello", cyan_bold))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fg_bg() {
|
||||
let cyan_fg_bg = Style::default().bg(Color::Cyan).fg(Color::Cyan);
|
||||
|
||||
assert_eq!("hello".cyan().on_cyan(), Span::styled("hello", cyan_fg_bg))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_attributes() {
|
||||
let cyan_bg = Style::default().bg(Color::Cyan);
|
||||
let cyan_fg = Style::default().fg(Color::Cyan);
|
||||
|
||||
// Behavior: the last one set is the definitive one
|
||||
assert_eq!("hello".on_red().on_cyan(), Span::styled("hello", cyan_bg));
|
||||
assert_eq!("hello".red().cyan(), Span::styled("hello", cyan_fg));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_chained() {
|
||||
let all_modifier_black = Style::default()
|
||||
.bg(Color::Black)
|
||||
.fg(Color::Black)
|
||||
.add_modifier(
|
||||
Modifier::UNDERLINED
|
||||
| Modifier::BOLD
|
||||
| Modifier::DIM
|
||||
| Modifier::SLOW_BLINK
|
||||
| Modifier::REVERSED
|
||||
| Modifier::CROSSED_OUT,
|
||||
);
|
||||
assert_eq!(
|
||||
"hello"
|
||||
.on_black()
|
||||
.black()
|
||||
.bold()
|
||||
.underline()
|
||||
.dimmed()
|
||||
.slow_blink()
|
||||
.crossed_out()
|
||||
.reversed(),
|
||||
Span::styled("hello", all_modifier_black)
|
||||
);
|
||||
}
|
||||
}
|
||||
102
src/symbols.rs
102
src/symbols.rs
@@ -1,3 +1,5 @@
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
pub mod block {
|
||||
pub const FULL: &str = "█";
|
||||
pub const SEVEN_EIGHTHS: &str = "▉";
|
||||
@@ -8,7 +10,7 @@ pub mod block {
|
||||
pub const ONE_QUARTER: &str = "▎";
|
||||
pub const ONE_EIGHTH: &str = "▏";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Set {
|
||||
pub full: &'static str,
|
||||
pub seven_eighths: &'static str,
|
||||
@@ -21,6 +23,12 @@ pub mod block {
|
||||
pub empty: &'static str,
|
||||
}
|
||||
|
||||
impl Default for Set {
|
||||
fn default() -> Self {
|
||||
NINE_LEVELS
|
||||
}
|
||||
}
|
||||
|
||||
pub const THREE_LEVELS: Set = Set {
|
||||
full: FULL,
|
||||
seven_eighths: FULL,
|
||||
@@ -56,7 +64,7 @@ pub mod bar {
|
||||
pub const ONE_QUARTER: &str = "▂";
|
||||
pub const ONE_EIGHTH: &str = "▁";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Set {
|
||||
pub full: &'static str,
|
||||
pub seven_eighths: &'static str,
|
||||
@@ -69,6 +77,12 @@ pub mod bar {
|
||||
pub empty: &'static str,
|
||||
}
|
||||
|
||||
impl Default for Set {
|
||||
fn default() -> Self {
|
||||
NINE_LEVELS
|
||||
}
|
||||
}
|
||||
|
||||
pub const THREE_LEVELS: Set = Set {
|
||||
full: FULL,
|
||||
seven_eighths: FULL,
|
||||
@@ -143,7 +157,7 @@ pub mod line {
|
||||
pub const DOUBLE_CROSS: &str = "╬";
|
||||
pub const THICK_CROSS: &str = "╋";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Set {
|
||||
pub vertical: &'static str,
|
||||
pub horizontal: &'static str,
|
||||
@@ -158,6 +172,12 @@ pub mod line {
|
||||
pub cross: &'static str,
|
||||
}
|
||||
|
||||
impl Default for Set {
|
||||
fn default() -> Self {
|
||||
NORMAL
|
||||
}
|
||||
}
|
||||
|
||||
pub const NORMAL: Set = Set {
|
||||
vertical: VERTICAL,
|
||||
horizontal: HORIZONTAL,
|
||||
@@ -222,9 +242,10 @@ pub mod braille {
|
||||
}
|
||||
|
||||
/// Marker to use when plotting data points
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum Marker {
|
||||
/// One point per cell in shape of dot
|
||||
#[default]
|
||||
Dot,
|
||||
/// One point per cell in shape of a block
|
||||
Block,
|
||||
@@ -233,3 +254,76 @@ pub enum Marker {
|
||||
/// Up to 8 points per cell
|
||||
Braille,
|
||||
}
|
||||
|
||||
pub mod scrollbar {
|
||||
use super::{block, line};
|
||||
|
||||
/// Scrollbar Set
|
||||
/// ```text
|
||||
/// <--▮------->
|
||||
/// ^ ^ ^ ^
|
||||
/// │ │ │ └ end
|
||||
/// │ │ └──── track
|
||||
/// │ └──────── thumb
|
||||
/// └─────────── begin
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Set {
|
||||
pub track: &'static str,
|
||||
pub thumb: &'static str,
|
||||
pub begin: &'static str,
|
||||
pub end: &'static str,
|
||||
}
|
||||
|
||||
pub const DOUBLE_VERTICAL: Set = Set {
|
||||
track: line::DOUBLE_VERTICAL,
|
||||
thumb: block::FULL,
|
||||
begin: "▲",
|
||||
end: "▼",
|
||||
};
|
||||
|
||||
pub const DOUBLE_HORIZONTAL: Set = Set {
|
||||
track: line::DOUBLE_HORIZONTAL,
|
||||
thumb: block::FULL,
|
||||
begin: "◄",
|
||||
end: "►",
|
||||
};
|
||||
|
||||
pub const VERTICAL: Set = Set {
|
||||
track: line::VERTICAL,
|
||||
thumb: block::FULL,
|
||||
begin: "↑",
|
||||
end: "↓",
|
||||
};
|
||||
|
||||
pub const HORIZONTAL: Set = Set {
|
||||
track: line::HORIZONTAL,
|
||||
thumb: block::FULL,
|
||||
begin: "←",
|
||||
end: "→",
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::ParseError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn marker_tostring() {
|
||||
assert_eq!(Marker::Dot.to_string(), "Dot");
|
||||
assert_eq!(Marker::Block.to_string(), "Block");
|
||||
assert_eq!(Marker::Bar.to_string(), "Bar");
|
||||
assert_eq!(Marker::Braille.to_string(), "Braille");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marker_from_str() {
|
||||
assert_eq!("Dot".parse::<Marker>(), Ok(Marker::Dot));
|
||||
assert_eq!("Block".parse::<Marker>(), Ok(Marker::Block));
|
||||
assert_eq!("Bar".parse::<Marker>(), Ok(Marker::Bar));
|
||||
assert_eq!("Braille".parse::<Marker>(), Ok(Marker::Braille));
|
||||
assert_eq!("".parse::<Marker>(), Err(ParseError::VariantNotFound));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::io;
|
||||
use std::{fmt, io};
|
||||
|
||||
use crate::{
|
||||
backend::{Backend, ClearType},
|
||||
@@ -7,22 +7,33 @@ use crate::{
|
||||
widgets::{StatefulWidget, Widget},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum Viewport {
|
||||
#[default]
|
||||
Fullscreen,
|
||||
Inline(u16),
|
||||
Fixed(Rect),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
impl fmt::Display for Viewport {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Viewport::Fullscreen => write!(f, "Fullscreen"),
|
||||
Viewport::Inline(height) => write!(f, "Inline({})", height),
|
||||
Viewport::Fixed(area) => write!(f, "Fixed({})", area),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Options to pass to [`Terminal::with_options`]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct TerminalOptions {
|
||||
/// Viewport used to draw to the terminal
|
||||
pub viewport: Viewport,
|
||||
}
|
||||
|
||||
/// Interface to the terminal backed by Termion
|
||||
#[derive(Debug)]
|
||||
/// Interface to the terminal backed by a [`Backend`].
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
@@ -46,6 +57,7 @@ where
|
||||
}
|
||||
|
||||
/// Represents a consistent terminal interface for rendering.
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct Frame<'a, B: 'a>
|
||||
where
|
||||
B: Backend,
|
||||
@@ -137,6 +149,7 @@ where
|
||||
/// `CompletedFrame` represents the state of the terminal after all changes performed in the last
|
||||
/// [`Terminal::draw`] call have been applied. Therefore, it is only valid until the next call to
|
||||
/// [`Terminal::draw`].
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct CompletedFrame<'a> {
|
||||
pub buffer: &'a Buffer,
|
||||
pub area: Rect,
|
||||
@@ -484,3 +497,18 @@ fn compute_inline_size<B: Backend>(
|
||||
pos,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn viewport_to_string() {
|
||||
assert_eq!(Viewport::Fullscreen.to_string(), "Fullscreen");
|
||||
assert_eq!(Viewport::Inline(5).to_string(), "Inline(5)");
|
||||
assert_eq!(
|
||||
Viewport::Fixed(Rect::new(0, 0, 5, 5)).to_string(),
|
||||
"Fixed(5x5+0+0)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
470
src/text.rs
470
src/text.rs
@@ -1,470 +0,0 @@
|
||||
//! Primitives for styled text.
|
||||
//!
|
||||
//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish,
|
||||
//! those strings may be associated to a set of styles. `ratatui` has three ways to represent them:
|
||||
//! - A single line string where all graphemes have the same style is represented by a [`Span`].
|
||||
//! - A single line string where each grapheme may have its own style is represented by [`Line`].
|
||||
//! - A multiple line string where each grapheme may have its own style is represented by a
|
||||
//! [`Text`].
|
||||
//!
|
||||
//! These types form a hierarchy: [`Line`] is a collection of [`Span`] and each line of [`Text`]
|
||||
//! is a [`Line`].
|
||||
//!
|
||||
//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is
|
||||
//! supported for their properties. Moreover, `ratatui` provides convenient `From` implementations
|
||||
//! so that you can start by using simple `String` or `&str` and then promote them to the previous
|
||||
//! primitives when you need additional styling capabilities.
|
||||
//!
|
||||
//! For example, for the [`crate::widgets::Block`] widget, all the following calls are valid to set
|
||||
//! its `title` property (which is a [`Line`] under the hood):
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use ratatui::widgets::Block;
|
||||
//! # use ratatui::text::{Span, Line};
|
||||
//! # use ratatui::style::{Color, Style};
|
||||
//! // A simple string with no styling.
|
||||
//! // Converted to Line(vec![
|
||||
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
|
||||
//! // ])
|
||||
//! let block = Block::default().title("My title");
|
||||
//!
|
||||
//! // A simple string with a unique style.
|
||||
//! // Converted to Line(vec![
|
||||
//! // Span { content: Cow::Borrowed("My title"), style: Style { fg: Some(Color::Yellow), .. }
|
||||
//! // ])
|
||||
//! let block = Block::default().title(
|
||||
//! Span::styled("My title", Style::default().fg(Color::Yellow))
|
||||
//! );
|
||||
//!
|
||||
//! // A string with multiple styles.
|
||||
//! // Converted to Line(vec![
|
||||
//! // Span { content: Cow::Borrowed("My"), style: Style { fg: Some(Color::Yellow), .. } },
|
||||
//! // Span { content: Cow::Borrowed(" title"), .. }
|
||||
//! // ])
|
||||
//! let block = Block::default().title(vec![
|
||||
//! Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
//! Span::raw(" title"),
|
||||
//! ]);
|
||||
//! ```
|
||||
use std::{borrow::Cow, fmt::Debug};
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::style::{Style, Styled};
|
||||
|
||||
mod line;
|
||||
mod masked;
|
||||
mod spans;
|
||||
#[allow(deprecated)]
|
||||
pub use {line::Line, masked::Masked, spans::Spans};
|
||||
|
||||
/// A grapheme associated to a style.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct StyledGrapheme<'a> {
|
||||
pub symbol: &'a str,
|
||||
pub style: Style,
|
||||
}
|
||||
|
||||
/// A string where all graphemes have the same style.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Span<'a> {
|
||||
pub content: Cow<'a, str>,
|
||||
pub style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Span<'a> {
|
||||
/// Create a span with no style.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Span;
|
||||
/// Span::raw("My text");
|
||||
/// Span::raw(String::from("My text"));
|
||||
/// ```
|
||||
pub fn raw<T>(content: T) -> Span<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
Span {
|
||||
content: content.into(),
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a span with a style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Span;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// Span::styled("My text", style);
|
||||
/// Span::styled(String::from("My text"), style);
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Span<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
Span {
|
||||
content: content.into(),
|
||||
style,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the width of the content held by this span.
|
||||
pub fn width(&self) -> usize {
|
||||
self.content.width()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the graphemes held by this span.
|
||||
///
|
||||
/// `base_style` is the [`Style`] that will be patched with each grapheme [`Style`] to get
|
||||
/// the resulting [`Style`].
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, StyledGrapheme};
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// # use std::iter::Iterator;
|
||||
/// let style = Style::default().fg(Color::Yellow);
|
||||
/// let span = Span::styled("Text", style);
|
||||
/// let style = Style::default().fg(Color::Green).bg(Color::Black);
|
||||
/// let styled_graphemes = span.styled_graphemes(style);
|
||||
/// assert_eq!(
|
||||
/// vec![
|
||||
/// StyledGrapheme {
|
||||
/// symbol: "T",
|
||||
/// style: Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Black),
|
||||
/// add_modifier: Modifier::empty(),
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// },
|
||||
/// StyledGrapheme {
|
||||
/// symbol: "e",
|
||||
/// style: Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Black),
|
||||
/// add_modifier: Modifier::empty(),
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// },
|
||||
/// StyledGrapheme {
|
||||
/// symbol: "x",
|
||||
/// style: Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Black),
|
||||
/// add_modifier: Modifier::empty(),
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// },
|
||||
/// StyledGrapheme {
|
||||
/// symbol: "t",
|
||||
/// style: Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Black),
|
||||
/// add_modifier: Modifier::empty(),
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// },
|
||||
/// ],
|
||||
/// styled_graphemes.collect::<Vec<StyledGrapheme>>()
|
||||
/// );
|
||||
/// ```
|
||||
pub fn styled_graphemes(
|
||||
&'a self,
|
||||
base_style: Style,
|
||||
) -> impl Iterator<Item = StyledGrapheme<'a>> {
|
||||
UnicodeSegmentation::graphemes(self.content.as_ref(), true)
|
||||
.map(move |g| StyledGrapheme {
|
||||
symbol: g,
|
||||
style: base_style.patch(self.style),
|
||||
})
|
||||
.filter(|s| s.symbol != "\n")
|
||||
}
|
||||
|
||||
/// Patches the style an existing Span, adding modifiers from the given style.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Span;
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut raw_span = Span::raw("My text");
|
||||
/// let mut styled_span = Span::styled("My text", style);
|
||||
///
|
||||
/// assert_ne!(raw_span, styled_span);
|
||||
///
|
||||
/// raw_span.patch_style(style);
|
||||
/// assert_eq!(raw_span, styled_span);
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
self.style = self.style.patch(style);
|
||||
}
|
||||
|
||||
/// Resets the style of the Span.
|
||||
/// Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Span;
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let mut span = Span::styled("My text", Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC));
|
||||
///
|
||||
/// span.reset_style();
|
||||
/// assert_eq!(Style::reset(), span.style);
|
||||
/// ```
|
||||
pub fn reset_style(&mut self) {
|
||||
self.patch_style(Style::reset());
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Span<'a> {
|
||||
fn from(s: String) -> Span<'a> {
|
||||
Span::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Span<'a> {
|
||||
fn from(s: &'a str) -> Span<'a> {
|
||||
Span::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Span<'a> {
|
||||
type Item = Span<'a>;
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self {
|
||||
Self::styled(self.content, style)
|
||||
}
|
||||
}
|
||||
|
||||
/// A string split over multiple lines where each line is composed of several clusters, each with
|
||||
/// their own style.
|
||||
///
|
||||
/// A [`Text`], like a [`Span`], can be constructed using one of the many `From` implementations
|
||||
/// or via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements
|
||||
/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Text;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
///
|
||||
/// // An initial two lines of `Text` built from a `&str`
|
||||
/// let mut text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(2, text.height());
|
||||
///
|
||||
/// // Adding two more unstyled lines
|
||||
/// text.extend(Text::raw("These are two\nmore lines!"));
|
||||
/// assert_eq!(4, text.height());
|
||||
///
|
||||
/// // Adding a final two styled lines
|
||||
/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
|
||||
/// assert_eq!(6, text.height());
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Default, Eq)]
|
||||
pub struct Text<'a> {
|
||||
pub lines: Vec<Line<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Text<'a> {
|
||||
/// Create some text (potentially multiple lines) with no style.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Text;
|
||||
/// Text::raw("The first line\nThe second line");
|
||||
/// Text::raw(String::from("The first line\nThe second line"));
|
||||
/// ```
|
||||
pub fn raw<T>(content: T) -> Text<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
let lines: Vec<_> = match content.into() {
|
||||
Cow::Borrowed("") => vec![Line::from("")],
|
||||
Cow::Borrowed(s) => s.lines().map(Line::from).collect(),
|
||||
Cow::Owned(s) if s.is_empty() => vec![Line::from("")],
|
||||
Cow::Owned(s) => s.lines().map(|l| Line::from(l.to_owned())).collect(),
|
||||
};
|
||||
|
||||
Text::from(lines)
|
||||
}
|
||||
|
||||
/// Create some text (potentially multiple lines) with a style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Text;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// Text::styled("The first line\nThe second line", style);
|
||||
/// Text::styled(String::from("The first line\nThe second line"), style);
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Text<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
let mut text = Text::raw(content);
|
||||
text.patch_style(style);
|
||||
text
|
||||
}
|
||||
|
||||
/// Returns the max width of all the lines.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::text::Text;
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(15, text.width());
|
||||
/// ```
|
||||
pub fn width(&self) -> usize {
|
||||
self.lines.iter().map(Line::width).max().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns the height.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::text::Text;
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(2, text.height());
|
||||
/// ```
|
||||
pub fn height(&self) -> usize {
|
||||
self.lines.len()
|
||||
}
|
||||
|
||||
/// Patches the style of each line in an existing Text, adding modifiers from the given style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Text;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut raw_text = Text::raw("The first line\nThe second line");
|
||||
/// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
|
||||
/// assert_ne!(raw_text, styled_text);
|
||||
///
|
||||
/// raw_text.patch_style(style);
|
||||
/// assert_eq!(raw_text, styled_text);
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
for line in &mut self.lines {
|
||||
line.patch_style(style);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the style of the Text.
|
||||
/// Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Line, Text};
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut text = Text::styled("The first line\nThe second line", style);
|
||||
///
|
||||
/// text.reset_style();
|
||||
/// for line in &text.lines {
|
||||
/// for span in &line.spans {
|
||||
/// assert_eq!(Style::reset(), span.style);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn reset_style(&mut self) {
|
||||
for line in &mut self.lines {
|
||||
line.reset_style();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Text<'a> {
|
||||
fn from(s: String) -> Text<'a> {
|
||||
Text::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Text<'a> {
|
||||
fn from(s: &'a str) -> Text<'a> {
|
||||
Text::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Cow<'a, str>> for Text<'a> {
|
||||
fn from(s: Cow<'a, str>) -> Text<'a> {
|
||||
Text::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Span<'a>> for Text<'a> {
|
||||
fn from(span: Span<'a>) -> Text<'a> {
|
||||
Text {
|
||||
lines: vec![Line::from(span)],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<'a> From<Spans<'a>> for Text<'a> {
|
||||
fn from(spans: Spans<'a>) -> Text<'a> {
|
||||
Text {
|
||||
lines: vec![spans.into()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Line<'a>> for Text<'a> {
|
||||
fn from(line: Line<'a>) -> Text<'a> {
|
||||
Text { lines: vec![line] }
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<'a> From<Vec<Spans<'a>>> for Text<'a> {
|
||||
fn from(lines: Vec<Spans<'a>>) -> Text<'a> {
|
||||
Text {
|
||||
lines: lines.into_iter().map(|l| l.0.into()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Vec<Line<'a>>> for Text<'a> {
|
||||
fn from(lines: Vec<Line<'a>>) -> Text<'a> {
|
||||
Text { lines }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for Text<'a> {
|
||||
type Item = Line<'a>;
|
||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.lines.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Extend<T> for Text<'a>
|
||||
where
|
||||
T: Into<Line<'a>>,
|
||||
{
|
||||
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
|
||||
let lines = iter.into_iter().map(Into::into);
|
||||
self.lines.extend(lines);
|
||||
}
|
||||
}
|
||||
67
src/text/grapheme.rs
Normal file
67
src/text/grapheme.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use crate::style::{Style, Styled};
|
||||
|
||||
/// A grapheme associated to a style.
|
||||
/// Note that, although `StyledGrapheme` is the smallest divisible unit of text,
|
||||
/// it actually is not a member of the text type hierarchy (`Text` -> `Line` -> `Span`).
|
||||
/// It is a separate type used mostly for rendering purposes. A `Span` consists of components that
|
||||
/// can be split into `StyledGrapheme`s, but it does not contain a collection of `StyledGrapheme`s.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct StyledGrapheme<'a> {
|
||||
pub symbol: &'a str,
|
||||
pub style: Style,
|
||||
}
|
||||
|
||||
impl<'a> StyledGrapheme<'a> {
|
||||
pub fn new(symbol: &'a str, style: Style) -> StyledGrapheme<'a> {
|
||||
StyledGrapheme { symbol, style }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for StyledGrapheme<'a> {
|
||||
type Item = StyledGrapheme<'a>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(mut self, style: Style) -> Self::Item {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
let style = Style::new().yellow();
|
||||
let sg = StyledGrapheme::new("a", style);
|
||||
assert_eq!(sg.symbol, "a");
|
||||
assert_eq!(sg.style, style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style() {
|
||||
let style = Style::new().yellow();
|
||||
let sg = StyledGrapheme::new("a", style);
|
||||
assert_eq!(sg.style(), style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_style() {
|
||||
let style = Style::new().yellow().on_red();
|
||||
let style2 = Style::new().green();
|
||||
let sg = StyledGrapheme::new("a", style).set_style(style2);
|
||||
assert_eq!(sg.style, style2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylize() {
|
||||
let style = Style::new().yellow().on_red();
|
||||
let sg = StyledGrapheme::new("a", style).green();
|
||||
assert_eq!(sg.style, Style::new().green().on_red());
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,34 @@
|
||||
#![allow(deprecated)]
|
||||
use super::{Span, Spans, Style};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use super::{Span, Spans, Style, StyledGrapheme};
|
||||
use crate::layout::Alignment;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, Eq)]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Line<'a> {
|
||||
pub spans: Vec<Span<'a>>,
|
||||
pub alignment: Option<Alignment>,
|
||||
}
|
||||
|
||||
impl<'a> Line<'a> {
|
||||
/// Create a line with a style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Line;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// Line::styled("My text", style);
|
||||
/// Line::styled(String::from("My text"), style);
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Line<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
Line::from(Span::styled(content, style))
|
||||
}
|
||||
|
||||
/// Returns the width of the underlying string.
|
||||
///
|
||||
/// ## Examples
|
||||
@@ -26,6 +46,38 @@ impl<'a> Line<'a> {
|
||||
self.spans.iter().map(Span::width).sum()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the graphemes held by this line.
|
||||
///
|
||||
/// `base_style` is the [`Style`] that will be patched with each grapheme [`Style`] to get
|
||||
/// the resulting [`Style`].
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Line, StyledGrapheme};
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// # use std::iter::Iterator;
|
||||
/// let line = Line::styled("Text", Style::default().fg(Color::Yellow));
|
||||
/// let style = Style::default().fg(Color::Green).bg(Color::Black);
|
||||
/// assert_eq!(
|
||||
/// line.styled_graphemes(style).collect::<Vec<StyledGrapheme>>(),
|
||||
/// vec![
|
||||
/// StyledGrapheme::new("T", Style::default().fg(Color::Yellow).bg(Color::Black)),
|
||||
/// StyledGrapheme::new("e", Style::default().fg(Color::Yellow).bg(Color::Black)),
|
||||
/// StyledGrapheme::new("x", Style::default().fg(Color::Yellow).bg(Color::Black)),
|
||||
/// StyledGrapheme::new("t", Style::default().fg(Color::Yellow).bg(Color::Black)),
|
||||
/// ]
|
||||
/// );
|
||||
/// ```
|
||||
pub fn styled_graphemes(
|
||||
&'a self,
|
||||
base_style: Style,
|
||||
) -> impl Iterator<Item = StyledGrapheme<'a>> {
|
||||
self.spans
|
||||
.iter()
|
||||
.flat_map(move |span| span.styled_graphemes(base_style))
|
||||
}
|
||||
|
||||
/// Patches the style of each Span in an existing Line, adding modifiers from the given style.
|
||||
///
|
||||
/// ## Examples
|
||||
@@ -146,7 +198,7 @@ mod tests {
|
||||
use crate::{
|
||||
layout::Alignment,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span, Spans},
|
||||
text::{Line, Span, Spans, StyledGrapheme},
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -248,4 +300,34 @@ mod tests {
|
||||
let line = Line::from("This is default");
|
||||
assert_eq!(None, line.alignment);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styled_graphemes() {
|
||||
const RED: Style = Style::new().fg(Color::Red);
|
||||
const GREEN: Style = Style::new().fg(Color::Green);
|
||||
const BLUE: Style = Style::new().fg(Color::Blue);
|
||||
const RED_ON_WHITE: Style = Style::new().fg(Color::Red).bg(Color::White);
|
||||
const GREEN_ON_WHITE: Style = Style::new().fg(Color::Green).bg(Color::White);
|
||||
const BLUE_ON_WHITE: Style = Style::new().fg(Color::Blue).bg(Color::White);
|
||||
|
||||
let line = Line::from(vec![
|
||||
Span::styled("He", RED),
|
||||
Span::styled("ll", GREEN),
|
||||
Span::styled("o!", BLUE),
|
||||
]);
|
||||
let styled_graphemes = line
|
||||
.styled_graphemes(Style::new().bg(Color::White))
|
||||
.collect::<Vec<StyledGrapheme>>();
|
||||
assert_eq!(
|
||||
styled_graphemes,
|
||||
vec![
|
||||
StyledGrapheme::new("H", RED_ON_WHITE),
|
||||
StyledGrapheme::new("e", RED_ON_WHITE),
|
||||
StyledGrapheme::new("l", GREEN_ON_WHITE),
|
||||
StyledGrapheme::new("l", GREEN_ON_WHITE),
|
||||
StyledGrapheme::new("o", BLUE_ON_WHITE),
|
||||
StyledGrapheme::new("!", BLUE_ON_WHITE),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ use super::Text;
|
||||
/// Paragraph::new(password).render(buffer.area, &mut buffer);
|
||||
/// assert_eq!(buffer, Buffer::with_lines(vec!["xxxxx"]));
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Masked<'a> {
|
||||
inner: Cow<'a, str>,
|
||||
mask_char: char,
|
||||
@@ -86,43 +86,58 @@ impl<'a> From<Masked<'a>> for Text<'a> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::borrow::Borrow;
|
||||
|
||||
use super::*;
|
||||
use crate::text::Line;
|
||||
|
||||
#[test]
|
||||
fn test_masked_value() {
|
||||
fn new() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(masked.inner, "12345");
|
||||
assert_eq!(masked.mask_char, 'x');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(masked.value(), "xxxxx");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_masked_debug() {
|
||||
fn mask_char() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(masked.mask_char(), 'x');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(format!("{masked:?}"), "12345");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_masked_display() {
|
||||
fn display() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(format!("{masked}"), "xxxxx");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_masked_conversions() {
|
||||
fn into_text() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
|
||||
let text: Text = masked.borrow().into();
|
||||
let text: Text = (&masked).into();
|
||||
assert_eq!(text.lines, vec![Line::from("xxxxx")]);
|
||||
|
||||
let text: Text = masked.to_owned().into();
|
||||
let text: Text = masked.into();
|
||||
assert_eq!(text.lines, vec![Line::from("xxxxx")]);
|
||||
}
|
||||
|
||||
let cow: Cow<str> = masked.borrow().into();
|
||||
#[test]
|
||||
fn into_cow() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
let cow: Cow<str> = (&masked).into();
|
||||
assert_eq!(cow, "xxxxx");
|
||||
|
||||
let cow: Cow<str> = masked.to_owned().into();
|
||||
let cow: Cow<str> = masked.into();
|
||||
assert_eq!(cow, "xxxxx");
|
||||
}
|
||||
}
|
||||
|
||||
71
src/text/mod.rs
Normal file
71
src/text/mod.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
//! Primitives for styled text.
|
||||
//!
|
||||
//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish,
|
||||
//! those strings may be associated to a set of styles. `ratatui` has three ways to represent them:
|
||||
//! - A single line string where all graphemes have the same style is represented by a [`Span`].
|
||||
//! - A single line string where each grapheme may have its own style is represented by [`Line`].
|
||||
//! - A multiple line string where each grapheme may have its own style is represented by a
|
||||
//! [`Text`].
|
||||
//!
|
||||
//! These types form a hierarchy: [`Line`] is a collection of [`Span`] and each line of [`Text`]
|
||||
//! is a [`Line`].
|
||||
//!
|
||||
//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is
|
||||
//! supported for their properties. Moreover, `ratatui` provides convenient `From` implementations
|
||||
//! so that you can start by using simple `String` or `&str` and then promote them to the previous
|
||||
//! primitives when you need additional styling capabilities.
|
||||
//!
|
||||
//! For example, for the [`crate::widgets::Block`] widget, all the following calls are valid to set
|
||||
//! its `title` property (which is a [`Line`] under the hood):
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use ratatui::widgets::Block;
|
||||
//! # use ratatui::text::{Span, Line};
|
||||
//! # use ratatui::style::{Color, Style};
|
||||
//! // A simple string with no styling.
|
||||
//! // Converted to Line(vec![
|
||||
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
|
||||
//! // ])
|
||||
//! let block = Block::default().title("My title");
|
||||
//!
|
||||
//! // A simple string with a unique style.
|
||||
//! // Converted to Line(vec![
|
||||
//! // Span { content: Cow::Borrowed("My title"), style: Style { fg: Some(Color::Yellow), .. }
|
||||
//! // ])
|
||||
//! let block = Block::default().title(
|
||||
//! Span::styled("My title", Style::default().fg(Color::Yellow))
|
||||
//! );
|
||||
//!
|
||||
//! // A string with multiple styles.
|
||||
//! // Converted to Line(vec![
|
||||
//! // Span { content: Cow::Borrowed("My"), style: Style { fg: Some(Color::Yellow), .. } },
|
||||
//! // Span { content: Cow::Borrowed(" title"), .. }
|
||||
//! // ])
|
||||
//! let block = Block::default().title(vec![
|
||||
//! Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
//! Span::raw(" title"),
|
||||
//! ]);
|
||||
//! ```
|
||||
|
||||
use crate::style::Style;
|
||||
|
||||
mod grapheme;
|
||||
pub use grapheme::StyledGrapheme;
|
||||
|
||||
mod line;
|
||||
pub use line::Line;
|
||||
|
||||
mod masked;
|
||||
pub use masked::Masked;
|
||||
|
||||
mod span;
|
||||
pub use span::Span;
|
||||
|
||||
/// We keep this for backward compatibility.
|
||||
mod spans;
|
||||
#[allow(deprecated)]
|
||||
pub use spans::Spans;
|
||||
|
||||
#[allow(clippy::module_inception)]
|
||||
mod text;
|
||||
pub use text::Text;
|
||||
302
src/text/span.rs
Normal file
302
src/text/span.rs
Normal file
@@ -0,0 +1,302 @@
|
||||
use std::{borrow::Cow, fmt::Debug};
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use super::StyledGrapheme;
|
||||
use crate::style::{Style, Styled};
|
||||
|
||||
/// Represents a part of a line that is contiguous and where all characters share the same style.
|
||||
///
|
||||
/// A `Span` is the smallest unit of text that can be styled. It is usually combined in the [`Line`]
|
||||
/// type to represent a line of text where each `Span` may have a different style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// A `Span` with `style` set to [`Style::default()`] can be created from a `&str`, a `String`, or
|
||||
/// any type convertible to [`Cow<str>`].
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let span = Span::raw("test content");
|
||||
/// let span = Span::raw(String::from("test content"));
|
||||
/// let span = Span::from("test content");
|
||||
/// let span = Span::from(String::from("test content"));
|
||||
/// let span: Span = "test content".into();
|
||||
/// let span: Span = String::from("test content").into();
|
||||
/// ```
|
||||
///
|
||||
/// Styled spans can be created using [`Span::styled`] or by converting strings using methods from
|
||||
/// the [`Stylize`] trait.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let span = Span::styled("test content", Style::new().green());
|
||||
/// let span = Span::styled(String::from("test content"), Style::new().green());
|
||||
/// let span = "test content".green();
|
||||
/// let span = String::from("test content").green();
|
||||
/// ```
|
||||
///
|
||||
/// `Span` implements [`Stylize`], which allows it to be styled using the shortcut methods. Styles
|
||||
/// applied are additive.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let span = Span::raw("test content").green().on_yellow().italic();
|
||||
/// let span = Span::raw(String::from("test content")).green().on_yellow().italic();
|
||||
/// ```
|
||||
///
|
||||
/// [`Line`]: crate::text::Line
|
||||
/// [`Stylize`]: crate::style::Stylize
|
||||
/// [`Cow<str>`]: std::borrow::Cow
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Span<'a> {
|
||||
/// The content of the span as a Clone-on-write string.
|
||||
pub content: Cow<'a, str>,
|
||||
/// The style of the span.
|
||||
pub style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Span<'a> {
|
||||
/// Create a span with the default style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Span;
|
||||
/// Span::raw("test content");
|
||||
/// Span::raw(String::from("test content"));
|
||||
/// ```
|
||||
pub fn raw<T>(content: T) -> Span<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
Span {
|
||||
content: content.into(),
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a span with the specified style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let style = Style::new().yellow().on_green().italic();
|
||||
/// Span::styled("test content", style);
|
||||
/// Span::styled(String::from("test content"), style);
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Span<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
Span {
|
||||
content: content.into(),
|
||||
style,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the unicode width of the content held by this span.
|
||||
pub fn width(&self) -> usize {
|
||||
self.content.width()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the graphemes held by this span.
|
||||
///
|
||||
/// `base_style` is the [`Style`] that will be patched with the `Span`'s `style` to get the
|
||||
/// resulting [`Style`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, text::StyledGrapheme};
|
||||
/// # use std::iter::Iterator;
|
||||
/// let span = Span::styled("Test", Style::new().green().italic());
|
||||
/// let style = Style::new().red().on_yellow();
|
||||
/// assert_eq!(
|
||||
/// span.styled_graphemes(style).collect::<Vec<StyledGrapheme>>(),
|
||||
/// vec![
|
||||
/// StyledGrapheme::new("T", Style::new().green().on_yellow().italic()),
|
||||
/// StyledGrapheme::new("e", Style::new().green().on_yellow().italic()),
|
||||
/// StyledGrapheme::new("s", Style::new().green().on_yellow().italic()),
|
||||
/// StyledGrapheme::new("t", Style::new().green().on_yellow().italic()),
|
||||
/// ],
|
||||
/// );
|
||||
/// ```
|
||||
pub fn styled_graphemes(
|
||||
&'a self,
|
||||
base_style: Style,
|
||||
) -> impl Iterator<Item = StyledGrapheme<'a>> {
|
||||
self.content
|
||||
.as_ref()
|
||||
.graphemes(true)
|
||||
.filter(|g| *g != "\n")
|
||||
.map(move |g| StyledGrapheme {
|
||||
symbol: g,
|
||||
style: base_style.patch(self.style),
|
||||
})
|
||||
}
|
||||
|
||||
/// Patches the style of the Span, adding modifiers from the given style.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut span = Span::styled("test content", Style::new().green().italic());
|
||||
/// span.patch_style(Style::new().red().on_yellow().bold());
|
||||
/// assert_eq!(span.style, Style::new().red().on_yellow().italic().bold());
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
self.style = self.style.patch(style);
|
||||
}
|
||||
|
||||
/// Resets the style of the Span.
|
||||
///
|
||||
/// This is Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let mut span = Span::styled("Test Content", Style::new().green().on_yellow().italic());
|
||||
/// span.reset_style();
|
||||
/// assert_eq!(span.style, Style::reset());
|
||||
/// ```
|
||||
pub fn reset_style(&mut self) {
|
||||
self.patch_style(Style::reset());
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for Span<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
fn from(s: T) -> Self {
|
||||
Span::raw(s.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Span<'a> {
|
||||
type Item = Span<'a>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::style::Stylize;
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
let span = Span::default();
|
||||
assert_eq!(span.content, Cow::Borrowed(""));
|
||||
assert_eq!(span.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_str() {
|
||||
let span = Span::raw("test content");
|
||||
assert_eq!(span.content, Cow::Borrowed("test content"));
|
||||
assert_eq!(span.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_string() {
|
||||
let content = String::from("test content");
|
||||
let span = Span::raw(content.clone());
|
||||
assert_eq!(span.content, Cow::Owned::<str>(content));
|
||||
assert_eq!(span.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styled_str() {
|
||||
let style = Style::new().red();
|
||||
let span = Span::styled("test content", style);
|
||||
assert_eq!(span.content, Cow::Borrowed("test content"));
|
||||
assert_eq!(span.style, Style::new().red());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styled_string() {
|
||||
let content = String::from("test content");
|
||||
let style = Style::new().green();
|
||||
let span = Span::styled(content.clone(), style);
|
||||
assert_eq!(span.content, Cow::Owned::<str>(content));
|
||||
assert_eq!(span.style, style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_ref_str_borrowed_cow() {
|
||||
let content = "test content";
|
||||
let span = Span::from(content);
|
||||
assert_eq!(span.content, Cow::Borrowed(content));
|
||||
assert_eq!(span.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_string_ref_str_borrowed_cow() {
|
||||
let content = String::from("test content");
|
||||
let span = Span::from(content.as_str());
|
||||
assert_eq!(span.content, Cow::Borrowed(content.as_str()));
|
||||
assert_eq!(span.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_string_owned_cow() {
|
||||
let content = String::from("test content");
|
||||
let span = Span::from(content.clone());
|
||||
assert_eq!(span.content, Cow::Owned::<str>(content));
|
||||
assert_eq!(span.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_ref_string_borrowed_cow() {
|
||||
let content = String::from("test content");
|
||||
let span = Span::from(&content);
|
||||
assert_eq!(span.content, Cow::Borrowed(content.as_str()));
|
||||
assert_eq!(span.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_style() {
|
||||
let mut span = Span::styled("test content", Style::new().green());
|
||||
span.reset_style();
|
||||
assert_eq!(span.style, Style::reset());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_style() {
|
||||
let mut span = Span::styled("test content", Style::new().green().on_yellow());
|
||||
span.patch_style(Style::new().red().bold());
|
||||
assert_eq!(span.style, Style::new().red().on_yellow().bold());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn width() {
|
||||
assert_eq!(Span::raw("").width(), 0);
|
||||
assert_eq!(Span::raw("test").width(), 4);
|
||||
assert_eq!(Span::raw("test content").width(), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylize() {
|
||||
let span = Span::raw("test content").green();
|
||||
assert_eq!(span.content, Cow::Borrowed("test content"));
|
||||
assert_eq!(span.style, Style::new().green());
|
||||
|
||||
let span = Span::styled("test content", Style::new().green());
|
||||
let stylized = span.on_yellow().bold();
|
||||
assert_eq!(stylized.content, Cow::Borrowed("test content"));
|
||||
assert_eq!(stylized.style, Style::new().green().on_yellow().bold());
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ use crate::{layout::Alignment, text::Line};
|
||||
/// future. All methods that accept Spans have been replaced with methods that
|
||||
/// accept Into<Line<'a>> (which is implemented on `Spans`) to allow users of
|
||||
/// this crate to gradually transition to Line.
|
||||
#[derive(Debug, Clone, PartialEq, Default, Eq)]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
#[deprecated(note = "Use `ratatui::text::Line` instead")]
|
||||
pub struct Spans<'a>(pub Vec<Span<'a>>);
|
||||
|
||||
|
||||
445
src/text/text.rs
Normal file
445
src/text/text.rs
Normal file
@@ -0,0 +1,445 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[allow(deprecated)]
|
||||
use super::{Line, Span, Spans};
|
||||
use crate::style::Style;
|
||||
|
||||
/// A string split over multiple lines where each line is composed of several clusters, each with
|
||||
/// their own style.
|
||||
///
|
||||
/// A [`Text`], like a [`Span`], can be constructed using one of the many `From` implementations
|
||||
/// or via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements
|
||||
/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Text;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
///
|
||||
/// // An initial two lines of `Text` built from a `&str`
|
||||
/// let mut text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(2, text.height());
|
||||
///
|
||||
/// // Adding two more unstyled lines
|
||||
/// text.extend(Text::raw("These are two\nmore lines!"));
|
||||
/// assert_eq!(4, text.height());
|
||||
///
|
||||
/// // Adding a final two styled lines
|
||||
/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
|
||||
/// assert_eq!(6, text.height());
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Text<'a> {
|
||||
pub lines: Vec<Line<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Text<'a> {
|
||||
/// Create some text (potentially multiple lines) with no style.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Text;
|
||||
/// Text::raw("The first line\nThe second line");
|
||||
/// Text::raw(String::from("The first line\nThe second line"));
|
||||
/// ```
|
||||
pub fn raw<T>(content: T) -> Text<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
let lines: Vec<_> = match content.into() {
|
||||
Cow::Borrowed("") => vec![Line::from("")],
|
||||
Cow::Borrowed(s) => s.lines().map(Line::from).collect(),
|
||||
Cow::Owned(s) if s.is_empty() => vec![Line::from("")],
|
||||
Cow::Owned(s) => s.lines().map(|l| Line::from(l.to_owned())).collect(),
|
||||
};
|
||||
|
||||
Text::from(lines)
|
||||
}
|
||||
|
||||
/// Create some text (potentially multiple lines) with a style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Text;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// Text::styled("The first line\nThe second line", style);
|
||||
/// Text::styled(String::from("The first line\nThe second line"), style);
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Text<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
let mut text = Text::raw(content);
|
||||
text.patch_style(style);
|
||||
text
|
||||
}
|
||||
|
||||
/// Returns the max width of all the lines.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::text::Text;
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(15, text.width());
|
||||
/// ```
|
||||
pub fn width(&self) -> usize {
|
||||
self.lines.iter().map(Line::width).max().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns the height.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::text::Text;
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(2, text.height());
|
||||
/// ```
|
||||
pub fn height(&self) -> usize {
|
||||
self.lines.len()
|
||||
}
|
||||
|
||||
/// Patches the style of each line in an existing Text, adding modifiers from the given style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Text;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut raw_text = Text::raw("The first line\nThe second line");
|
||||
/// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
|
||||
/// assert_ne!(raw_text, styled_text);
|
||||
///
|
||||
/// raw_text.patch_style(style);
|
||||
/// assert_eq!(raw_text, styled_text);
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
for line in &mut self.lines {
|
||||
line.patch_style(style);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the style of the Text.
|
||||
/// Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Line, Text};
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut text = Text::styled("The first line\nThe second line", style);
|
||||
///
|
||||
/// text.reset_style();
|
||||
/// for line in &text.lines {
|
||||
/// for span in &line.spans {
|
||||
/// assert_eq!(Style::reset(), span.style);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn reset_style(&mut self) {
|
||||
for line in &mut self.lines {
|
||||
line.reset_style();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Text<'a> {
|
||||
fn from(s: String) -> Text<'a> {
|
||||
Text::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Text<'a> {
|
||||
fn from(s: &'a str) -> Text<'a> {
|
||||
Text::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Cow<'a, str>> for Text<'a> {
|
||||
fn from(s: Cow<'a, str>) -> Text<'a> {
|
||||
Text::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Span<'a>> for Text<'a> {
|
||||
fn from(span: Span<'a>) -> Text<'a> {
|
||||
Text {
|
||||
lines: vec![Line::from(span)],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<'a> From<Spans<'a>> for Text<'a> {
|
||||
fn from(spans: Spans<'a>) -> Text<'a> {
|
||||
Text {
|
||||
lines: vec![spans.into()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Line<'a>> for Text<'a> {
|
||||
fn from(line: Line<'a>) -> Text<'a> {
|
||||
Text { lines: vec![line] }
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<'a> From<Vec<Spans<'a>>> for Text<'a> {
|
||||
fn from(lines: Vec<Spans<'a>>) -> Text<'a> {
|
||||
Text {
|
||||
lines: lines.into_iter().map(|l| l.0.into()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Vec<Line<'a>>> for Text<'a> {
|
||||
fn from(lines: Vec<Line<'a>>) -> Text<'a> {
|
||||
Text { lines }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for Text<'a> {
|
||||
type Item = Line<'a>;
|
||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.lines.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Extend<T> for Text<'a>
|
||||
where
|
||||
T: Into<Line<'a>>,
|
||||
{
|
||||
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
|
||||
let lines = iter.into_iter().map(Into::into);
|
||||
self.lines.extend(lines);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::style::Stylize;
|
||||
|
||||
#[test]
|
||||
fn raw() {
|
||||
let text = Text::raw("The first line\nThe second line");
|
||||
assert_eq!(
|
||||
text.lines,
|
||||
vec![Line::from("The first line"), Line::from("The second line")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styled() {
|
||||
let style = Style::new().yellow().italic();
|
||||
let text = Text::styled("The first line\nThe second line", style);
|
||||
assert_eq!(
|
||||
text.lines,
|
||||
vec![
|
||||
Line::from(Span::styled("The first line", style)),
|
||||
Line::from(Span::styled("The second line", style))
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn width() {
|
||||
let text = Text::from("The first line\nThe second line");
|
||||
assert_eq!(15, text.width());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn height() {
|
||||
let text = Text::from("The first line\nThe second line");
|
||||
assert_eq!(2, text.height());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_style() {
|
||||
let style = Style::new().yellow().italic();
|
||||
let style2 = Style::new().red().underlined();
|
||||
let mut text = Text::styled("The first line\nThe second line", style);
|
||||
|
||||
text.patch_style(style2);
|
||||
let expected_style = Style::new().red().italic().underlined();
|
||||
assert_eq!(
|
||||
text.lines,
|
||||
vec![
|
||||
Line::from(Span::styled("The first line", expected_style)),
|
||||
Line::from(Span::styled("The second line", expected_style))
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_style() {
|
||||
let style = Style::new().yellow().italic();
|
||||
let mut text = Text::styled("The first line\nThe second line", style);
|
||||
|
||||
text.reset_style();
|
||||
assert_eq!(
|
||||
text.lines,
|
||||
vec![
|
||||
Line::from(Span::styled("The first line", Style::reset())),
|
||||
Line::from(Span::styled("The second line", Style::reset()))
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_string() {
|
||||
let text = Text::from(String::from("The first line\nThe second line"));
|
||||
assert_eq!(
|
||||
text.lines,
|
||||
vec![Line::from("The first line"), Line::from("The second line")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
let text = Text::from("The first line\nThe second line");
|
||||
assert_eq!(
|
||||
text.lines,
|
||||
vec![Line::from("The first line"), Line::from("The second line")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_cow() {
|
||||
let text = Text::from(Cow::Borrowed("The first line\nThe second line"));
|
||||
assert_eq!(
|
||||
text.lines,
|
||||
vec![Line::from("The first line"), Line::from("The second line")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_span() {
|
||||
let style = Style::new().yellow().italic();
|
||||
let text = Text::from(Span::styled("The first line\nThe second line", style));
|
||||
assert_eq!(
|
||||
text.lines,
|
||||
vec![Line::from(Span::styled(
|
||||
"The first line\nThe second line",
|
||||
style
|
||||
))]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn from_spans() {
|
||||
let style = Style::new().yellow().italic();
|
||||
let text = Text::from(Spans::from(vec![
|
||||
Span::styled("The first line", style),
|
||||
Span::styled("The second line", style),
|
||||
]));
|
||||
assert_eq!(
|
||||
text.lines,
|
||||
vec![Line::from(Spans::from(vec![
|
||||
Span::styled("The first line", style),
|
||||
Span::styled("The second line", style),
|
||||
]))]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_line() {
|
||||
let text = Text::from(Line::from("The first line"));
|
||||
assert_eq!(text.lines, vec![Line::from("The first line")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn from_vec_spans() {
|
||||
let text = Text::from(vec![
|
||||
Spans::from("The first line"),
|
||||
Spans::from("The second line"),
|
||||
]);
|
||||
assert_eq!(
|
||||
text.lines,
|
||||
vec![Line::from("The first line"), Line::from("The second line"),]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_vec_line() {
|
||||
let text = Text::from(vec![
|
||||
Line::from("The first line"),
|
||||
Line::from("The second line"),
|
||||
]);
|
||||
assert_eq!(
|
||||
text.lines,
|
||||
vec![Line::from("The first line"), Line::from("The second line")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_iter() {
|
||||
let text = Text::from("The first line\nThe second line");
|
||||
let mut iter = text.into_iter();
|
||||
assert_eq!(iter.next(), Some(Line::from("The first line")));
|
||||
assert_eq!(iter.next(), Some(Line::from("The second line")));
|
||||
assert_eq!(iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extend() {
|
||||
let mut text = Text::from("The first line\nThe second line");
|
||||
text.extend(vec![
|
||||
Line::from("The third line"),
|
||||
Line::from("The fourth line"),
|
||||
]);
|
||||
assert_eq!(
|
||||
text.lines,
|
||||
vec![
|
||||
Line::from("The first line"),
|
||||
Line::from("The second line"),
|
||||
Line::from("The third line"),
|
||||
Line::from("The fourth line"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extend_from_iter() {
|
||||
let mut text = Text::from("The first line\nThe second line");
|
||||
text.extend(vec![
|
||||
Line::from("The third line"),
|
||||
Line::from("The fourth line"),
|
||||
]);
|
||||
assert_eq!(
|
||||
text.lines,
|
||||
vec![
|
||||
Line::from("The first line"),
|
||||
Line::from("The second line"),
|
||||
Line::from("The third line"),
|
||||
Line::from("The fourth line"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extend_from_iter_str() {
|
||||
let mut text = Text::from("The first line\nThe second line");
|
||||
text.extend(vec!["The third line", "The fourth line"]);
|
||||
assert_eq!(
|
||||
text.lines,
|
||||
vec![
|
||||
Line::from("The first line"),
|
||||
Line::from("The second line"),
|
||||
Line::from("The third line"),
|
||||
Line::from("The fourth line"),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
103
src/title.rs
103
src/title.rs
@@ -1,23 +1,92 @@
|
||||
//! This module holds the [`Title`] element and its related configuration types.
|
||||
//! A title is a piece of [`Block`](crate::widgets::Block) configuration.
|
||||
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::{layout::Alignment, text::Line};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// A [`Block`](crate::widgets::Block) title.
|
||||
///
|
||||
/// It can be aligned (see [`Alignment`]) and positioned (see [`Position`]).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Title with no style.
|
||||
/// ```
|
||||
/// # use ratatui::widgets::block::Title;
|
||||
/// Title::from("Title");
|
||||
/// ```
|
||||
///
|
||||
/// Blue title on a white background (via [`Stylize`](crate::style::Stylize) trait).
|
||||
/// ```
|
||||
/// # use ratatui::widgets::block::Title;
|
||||
/// # use ratatui::style::Stylize;
|
||||
/// Title::from("Title".blue().on_white());
|
||||
/// ```
|
||||
///
|
||||
/// Title with multiple styles (see [`Line`] and [`Stylize`](crate::style::Stylize)).
|
||||
/// ```
|
||||
/// # use ratatui::widgets::block::Title;
|
||||
/// # use ratatui::style::Stylize;
|
||||
/// # use ratatui::text::Line;
|
||||
/// Title::from(
|
||||
/// Line::from(vec!["Q".white().underlined(), "uit".gray()])
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// Complete example
|
||||
/// ```
|
||||
/// # use ratatui::widgets::block::{Title, Position};
|
||||
/// # use ratatui::layout::Alignment;
|
||||
/// Title::from("Title")
|
||||
/// .position(Position::Top)
|
||||
/// .alignment(Alignment::Right);
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Title<'a> {
|
||||
/// Title content
|
||||
pub content: Line<'a>,
|
||||
/// Defaults to Left if unset
|
||||
/// Title alignment
|
||||
///
|
||||
/// If [`None`], defaults to the alignment defined with
|
||||
/// [`Block::title_alignment`](crate::widgets::Block::title_alignment) in the associated
|
||||
/// [`Block`](crate::widgets::Block).
|
||||
pub alignment: Option<Alignment>,
|
||||
|
||||
/// Defaults to Top if unset
|
||||
/// Title position
|
||||
///
|
||||
/// If [`None`], defaults to the position defined with
|
||||
/// [`Block::title_position`](crate::widgets::Block::title_position) in the associated
|
||||
/// [`Block`](crate::widgets::Block).
|
||||
pub position: Option<Position>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
/// Defines the [title](crate::widgets::block::Title) position.
|
||||
///
|
||||
/// The title can be positioned on top or at the bottom of the block.
|
||||
/// Defaults to [`Position::Top`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::widgets::{Block, block::{Title, Position}};
|
||||
/// Block::new().title(
|
||||
/// Title::from("title").position(Position::Bottom)
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Position {
|
||||
/// Position the title at the top of the block.
|
||||
///
|
||||
/// This is the default.
|
||||
#[default]
|
||||
Top,
|
||||
/// Position the title at the bottom of the block.
|
||||
Bottom,
|
||||
}
|
||||
|
||||
impl<'a> Title<'a> {
|
||||
/// Builder pattern method for setting the title content.
|
||||
pub fn content<T>(mut self, content: T) -> Title<'a>
|
||||
where
|
||||
T: Into<Line<'a>>,
|
||||
@@ -26,11 +95,13 @@ impl<'a> Title<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder pattern method for setting the title alignment.
|
||||
pub fn alignment(mut self, alignment: Alignment) -> Title<'a> {
|
||||
self.alignment = Some(alignment);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder pattern method for setting the title position.
|
||||
pub fn position(mut self, position: Position) -> Title<'a> {
|
||||
self.position = Some(position);
|
||||
self
|
||||
@@ -46,12 +117,22 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Default for Title<'a> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
content: Line::from(""),
|
||||
alignment: Some(Alignment::Left),
|
||||
position: Some(Position::Top),
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::ParseError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn position_tostring() {
|
||||
assert_eq!(Position::Top.to_string(), "Top");
|
||||
assert_eq!(Position::Bottom.to_string(), "Bottom");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn position_from_str() {
|
||||
assert_eq!("Top".parse::<Position>(), Ok(Position::Top));
|
||||
assert_eq!("Bottom".parse::<Position>(), Ok(Position::Bottom));
|
||||
assert_eq!("".parse::<Position>(), Err(ParseError::VariantNotFound));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
use std::cmp::min;
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
symbols,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
/// Display multiple bars in a single widgets
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::widgets::{Block, Borders, BarChart};
|
||||
/// # use ratatui::style::{Style, Color, Modifier};
|
||||
/// BarChart::default()
|
||||
/// .block(Block::default().title("BarChart").borders(Borders::ALL))
|
||||
/// .bar_width(3)
|
||||
/// .bar_gap(1)
|
||||
/// .bar_style(Style::default().fg(Color::Yellow).bg(Color::Red))
|
||||
/// .value_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD))
|
||||
/// .label_style(Style::default().fg(Color::White))
|
||||
/// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
|
||||
/// .max(4);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BarChart<'a> {
|
||||
/// Block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
/// The width of each bar
|
||||
bar_width: u16,
|
||||
/// The gap between each bar
|
||||
bar_gap: u16,
|
||||
/// Set of symbols used to display the data
|
||||
bar_set: symbols::bar::Set,
|
||||
/// Style of the bars
|
||||
bar_style: Style,
|
||||
/// Style of the values printed at the bottom of each bar
|
||||
value_style: Style,
|
||||
/// Style of the labels printed under each bar
|
||||
label_style: Style,
|
||||
/// Style for the widget
|
||||
style: Style,
|
||||
/// Slice of (label, value) pair to plot on the chart
|
||||
data: &'a [(&'a str, u64)],
|
||||
/// Value necessary for a bar to reach the maximum height (if no value is specified,
|
||||
/// the maximum value in the data is taken as reference)
|
||||
max: Option<u64>,
|
||||
/// Values to display on the bar (computed when the data is passed to the widget)
|
||||
values: Vec<String>,
|
||||
}
|
||||
|
||||
impl<'a> Default for BarChart<'a> {
|
||||
fn default() -> BarChart<'a> {
|
||||
BarChart {
|
||||
block: None,
|
||||
max: None,
|
||||
data: &[],
|
||||
values: Vec::new(),
|
||||
bar_style: Style::default(),
|
||||
bar_width: 1,
|
||||
bar_gap: 1,
|
||||
bar_set: symbols::bar::NINE_LEVELS,
|
||||
value_style: Style::default(),
|
||||
label_style: Style::default(),
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> BarChart<'a> {
|
||||
pub fn data(mut self, data: &'a [(&'a str, u64)]) -> BarChart<'a> {
|
||||
self.data = data;
|
||||
self.values = Vec::with_capacity(self.data.len());
|
||||
for &(_, v) in self.data {
|
||||
self.values.push(format!("{v}"));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn block(mut self, block: Block<'a>) -> BarChart<'a> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn max(mut self, max: u64) -> BarChart<'a> {
|
||||
self.max = Some(max);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bar_style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.bar_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bar_width(mut self, width: u16) -> BarChart<'a> {
|
||||
self.bar_width = width;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bar_gap(mut self, gap: u16) -> BarChart<'a> {
|
||||
self.bar_gap = gap;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn bar_set(mut self, bar_set: symbols::bar::Set) -> BarChart<'a> {
|
||||
self.bar_set = bar_set;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn value_style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.value_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn label_style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.label_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> BarChart<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for BarChart<'a> {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
|
||||
let chart_area = match self.block.take() {
|
||||
Some(b) => {
|
||||
let inner_area = b.inner(area);
|
||||
b.render(area, buf);
|
||||
inner_area
|
||||
}
|
||||
None => area,
|
||||
};
|
||||
|
||||
if chart_area.height < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let max = self
|
||||
.max
|
||||
.unwrap_or_else(|| self.data.iter().map(|t| t.1).max().unwrap_or_default());
|
||||
let max_index = min(
|
||||
(chart_area.width / (self.bar_width + self.bar_gap)) as usize,
|
||||
self.data.len(),
|
||||
);
|
||||
let mut data = self
|
||||
.data
|
||||
.iter()
|
||||
.take(max_index)
|
||||
.map(|&(l, v)| {
|
||||
(
|
||||
l,
|
||||
v * u64::from(chart_area.height - 1) * 8 / std::cmp::max(max, 1),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<(&str, u64)>>();
|
||||
for j in (0..chart_area.height - 1).rev() {
|
||||
for (i, d) in data.iter_mut().enumerate() {
|
||||
let symbol = match d.1 {
|
||||
0 => self.bar_set.empty,
|
||||
1 => self.bar_set.one_eighth,
|
||||
2 => self.bar_set.one_quarter,
|
||||
3 => self.bar_set.three_eighths,
|
||||
4 => self.bar_set.half,
|
||||
5 => self.bar_set.five_eighths,
|
||||
6 => self.bar_set.three_quarters,
|
||||
7 => self.bar_set.seven_eighths,
|
||||
_ => self.bar_set.full,
|
||||
};
|
||||
|
||||
for x in 0..self.bar_width {
|
||||
buf.get_mut(
|
||||
chart_area.left() + i as u16 * (self.bar_width + self.bar_gap) + x,
|
||||
chart_area.top() + j,
|
||||
)
|
||||
.set_symbol(symbol)
|
||||
.set_style(self.bar_style);
|
||||
}
|
||||
|
||||
if d.1 > 8 {
|
||||
d.1 -= 8;
|
||||
} else {
|
||||
d.1 = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (i, &(label, value)) in self.data.iter().take(max_index).enumerate() {
|
||||
if value != 0 {
|
||||
let value_label = &self.values[i];
|
||||
let width = value_label.width() as u16;
|
||||
if width < self.bar_width {
|
||||
buf.set_string(
|
||||
chart_area.left()
|
||||
+ i as u16 * (self.bar_width + self.bar_gap)
|
||||
+ (self.bar_width - width) / 2,
|
||||
chart_area.bottom() - 2,
|
||||
value_label,
|
||||
self.value_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
buf.set_stringn(
|
||||
chart_area.left() + i as u16 * (self.bar_width + self.bar_gap),
|
||||
chart_area.bottom() - 1,
|
||||
label,
|
||||
self.bar_width as usize,
|
||||
self.label_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
182
src/widgets/barchart/bar.rs
Normal file
182
src/widgets/barchart/bar.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
use crate::{buffer::Buffer, prelude::Rect, style::Style, text::Line};
|
||||
|
||||
/// A bar to be shown by the [`BarChart`](crate::widgets::BarChart) widget.
|
||||
///
|
||||
/// Here is an explanation of a `Bar`'s components.
|
||||
/// ```plain
|
||||
/// ███ ┐
|
||||
/// █2█ <- text_value or value │ bar
|
||||
/// foo <- label ┘
|
||||
/// ```
|
||||
/// Note that every element can be styled individually.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// The following example creates a bar with the label "Bar 1", a value "10",
|
||||
/// red background and a white value foreground.
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// Bar::default()
|
||||
/// .label("Bar 1".into())
|
||||
/// .value(10)
|
||||
/// .style(Style::default().fg(Color::Red))
|
||||
/// .value_style(Style::default().bg(Color::Red).fg(Color::White))
|
||||
/// .text_value("10°C".to_string());
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Bar<'a> {
|
||||
/// Value to display on the bar (computed when the data is passed to the widget)
|
||||
pub(super) value: u64,
|
||||
/// optional label to be printed under the bar
|
||||
pub(super) label: Option<Line<'a>>,
|
||||
/// style for the bar
|
||||
pub(super) style: Style,
|
||||
/// style of the value printed at the bottom of the bar.
|
||||
pub(super) value_style: Style,
|
||||
/// optional text_value to be shown on the bar instead of the actual value
|
||||
pub(super) text_value: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> Bar<'a> {
|
||||
/// Set the value of this bar.
|
||||
///
|
||||
/// The value will be displayed inside the bar.
|
||||
///
|
||||
/// # See also
|
||||
///
|
||||
/// [`Bar::value_style`] to style the value.
|
||||
/// [`Bar::text_value`] to set the displayed value.
|
||||
pub fn value(mut self, value: u64) -> Bar<'a> {
|
||||
self.value = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the label of the bar.
|
||||
///
|
||||
/// For [`Vertical`](crate::layout::Direction::Vertical) bars,
|
||||
/// display the label **under** the bar.
|
||||
/// For [`Horizontal`](crate::layout::Direction::Horizontal) bars,
|
||||
/// display the label **in** the bar.
|
||||
/// See [`BarChart::direction`](crate::widgets::BarChart::direction) to set the direction.
|
||||
pub fn label(mut self, label: Line<'a>) -> Bar<'a> {
|
||||
self.label = Some(label);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the style of the bar.
|
||||
///
|
||||
/// This will apply to every non-styled element.
|
||||
/// It can be seen and used as a default value.
|
||||
pub fn style(mut self, style: Style) -> Bar<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the style of the value.
|
||||
///
|
||||
/// # See also
|
||||
///
|
||||
/// [`Bar::value`] to set the value.
|
||||
pub fn value_style(mut self, style: Style) -> Bar<'a> {
|
||||
self.value_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the text value printed in the bar.
|
||||
///
|
||||
/// If `text_value` is not set, then the [ToString] representation of `value` will be shown on
|
||||
/// the bar.
|
||||
///
|
||||
/// # See also
|
||||
///
|
||||
/// [`Bar::value`] to set the value.
|
||||
pub fn text_value(mut self, text_value: String) -> Bar<'a> {
|
||||
self.text_value = Some(text_value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Render the value of the bar.
|
||||
///
|
||||
/// [`text_value`](Bar::text_value) is used if set, otherwise the value is converted to string.
|
||||
/// The value is rendered using value_style. If the value width is greater than the
|
||||
/// bar width, then the value is split into 2 parts. the first part is rendered in the bar
|
||||
/// using value_style. The second part is rendered outside the bar using bar_style
|
||||
pub(super) fn render_value_with_different_styles(
|
||||
self,
|
||||
buf: &mut Buffer,
|
||||
area: Rect,
|
||||
bar_length: usize,
|
||||
default_value_style: Style,
|
||||
bar_style: Style,
|
||||
) {
|
||||
let text = if let Some(text) = self.text_value {
|
||||
text
|
||||
} else {
|
||||
self.value.to_string()
|
||||
};
|
||||
|
||||
if !text.is_empty() {
|
||||
let style = default_value_style.patch(self.value_style);
|
||||
// Since the value may be longer than the bar itself, we need to use 2 different styles
|
||||
// while rendering. Render the first part with the default value style
|
||||
buf.set_stringn(area.x, area.y, &text, bar_length, style);
|
||||
// render the second part with the bar_style
|
||||
if text.len() > bar_length {
|
||||
let (first, second) = text.split_at(bar_length);
|
||||
|
||||
let style = bar_style.patch(self.style);
|
||||
buf.set_stringn(
|
||||
area.x + first.len() as u16,
|
||||
area.y,
|
||||
second,
|
||||
area.width as usize - first.len(),
|
||||
style,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn render_label_and_value(
|
||||
self,
|
||||
buf: &mut Buffer,
|
||||
max_width: u16,
|
||||
x: u16,
|
||||
y: u16,
|
||||
default_value_style: Style,
|
||||
default_label_style: Style,
|
||||
) {
|
||||
// render the value
|
||||
if self.value != 0 {
|
||||
let value_label = if let Some(text) = self.text_value {
|
||||
text
|
||||
} else {
|
||||
self.value.to_string()
|
||||
};
|
||||
|
||||
let width = value_label.len() as u16;
|
||||
if width < max_width {
|
||||
buf.set_string(
|
||||
x + (max_width.saturating_sub(value_label.len() as u16) >> 1),
|
||||
y,
|
||||
value_label,
|
||||
default_value_style.patch(self.value_style),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// render the label
|
||||
if let Some(mut label) = self.label {
|
||||
// patch label styles
|
||||
for span in &mut label.spans {
|
||||
span.style = default_label_style.patch(span.style);
|
||||
}
|
||||
|
||||
buf.set_line(
|
||||
x + (max_width.saturating_sub(label.width() as u16) >> 1),
|
||||
y + 1,
|
||||
&label,
|
||||
max_width,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
85
src/widgets/barchart/bar_group.rs
Normal file
85
src/widgets/barchart/bar_group.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use super::Bar;
|
||||
use crate::{
|
||||
prelude::{Alignment, Buffer, Rect},
|
||||
style::Style,
|
||||
text::Line,
|
||||
};
|
||||
|
||||
/// A group of bars to be shown by the Barchart.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// BarGroup::default()
|
||||
/// .label("Group 1".into())
|
||||
/// .bars(&[Bar::default().value(200), Bar::default().value(150)]);
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct BarGroup<'a> {
|
||||
/// label of the group. It will be printed centered under this group of bars
|
||||
pub(super) label: Option<Line<'a>>,
|
||||
/// list of bars to be shown
|
||||
pub(super) bars: Vec<Bar<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> BarGroup<'a> {
|
||||
/// Set the group label
|
||||
pub fn label(mut self, label: Line<'a>) -> BarGroup<'a> {
|
||||
self.label = Some(label);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the bars of the group to be shown
|
||||
pub fn bars(mut self, bars: &[Bar<'a>]) -> BarGroup<'a> {
|
||||
self.bars = bars.to_vec();
|
||||
self
|
||||
}
|
||||
|
||||
/// The maximum bar value of this group
|
||||
pub(super) fn max(&self) -> Option<u64> {
|
||||
self.bars.iter().max_by_key(|v| v.value).map(|v| v.value)
|
||||
}
|
||||
|
||||
pub(super) fn render_label(self, buf: &mut Buffer, area: Rect, default_label_style: Style) {
|
||||
if let Some(mut label) = self.label {
|
||||
// patch label styles
|
||||
for span in &mut label.spans {
|
||||
span.style = default_label_style.patch(span.style);
|
||||
}
|
||||
|
||||
let x_offset = match label.alignment {
|
||||
Some(Alignment::Center) => area.width.saturating_sub(label.width() as u16) >> 1,
|
||||
Some(Alignment::Right) => area.width.saturating_sub(label.width() as u16),
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
buf.set_line(area.x + x_offset, area.y, &label, area.width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&[(&'a str, u64)]> for BarGroup<'a> {
|
||||
fn from(value: &[(&'a str, u64)]) -> BarGroup<'a> {
|
||||
BarGroup {
|
||||
label: None,
|
||||
bars: value
|
||||
.iter()
|
||||
.map(|&(text, v)| Bar::default().value(v).label(text.into()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, const N: usize> From<&[(&'a str, u64); N]> for BarGroup<'a> {
|
||||
fn from(value: &[(&'a str, u64); N]) -> BarGroup<'a> {
|
||||
Self::from(value.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&Vec<(&'a str, u64)>> for BarGroup<'a> {
|
||||
fn from(value: &Vec<(&'a str, u64)>) -> BarGroup<'a> {
|
||||
let array: &[(&str, u64)] = value;
|
||||
Self::from(array)
|
||||
}
|
||||
}
|
||||
1153
src/widgets/barchart/mod.rs
Normal file
1153
src/widgets/barchart/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user